diff --git a/assets/bifocal-pidgin.png b/assets/bifocal-pidgin.png new file mode 100644 index 0000000..556127c Binary files /dev/null and b/assets/bifocal-pidgin.png differ diff --git a/library.json b/library.json index 615c57e..e1dd46b 100644 --- a/library.json +++ b/library.json @@ -1,7 +1,7 @@ { "name": "ESP32-Chimera-Core", - "description": "Alternate library M5Stack/Odroid-Go/D-Duino and possiblly other ESP32/TFT/SD bundles", - "keywords": ["ESP32-Chimera-Core", "M5Stack", "M5Core2", "Odroid-Go", "ttgo-ts", "d-duino-32-xs", "esp32-wrover-kit"], + "description": "Alternate library for M5Stack/Odroid-Go/D-Duino and possiblly other ESP32/TFT/SD bundles", + "keywords": ["ESP32-Chimera-Core", "M5Stack", "M5Core2", "Odroid-Go", "ttgo-ts", "d-duino-32-xs", "esp32-wrover-kit", "M5CoreS3", "T-Deck"], "authors": [ { "name": "tobozo", @@ -22,7 +22,7 @@ "name": "LovyanGFX" } ], - "version": "1.5.1", + "version": "1.5.2", "framework": "arduino", "platforms": "espressif32", "headers": "ESP32-Chimera-Core.h", diff --git a/library.properties b/library.properties index ea81a06..bbe2f68 100644 --- a/library.properties +++ b/library.properties @@ -1,8 +1,8 @@ name=ESP32-Chimera-Core -version=1.5.1 +version=1.5.2 author=tobozo,Lovyan03 maintainer=tobozo@noreply.github.com -sentence=Alternate library M5Stack/M5Core2/Odroid-Go/D-Duino and possiblly other ESP32/TFT/SD bundles +sentence=Alternate library for M5Stack/M5Core2/Odroid-Go/D-Duino and possiblly other ESP32/TFT/SD bundles paragraph=See more on https://github.com/tobozo/ESP32-Chimera-Core category=Device Control url=https://github.com/tobozo/ESP32-Chimera-Core diff --git a/src/Config.h b/src/Config.h index 69e90e1..45a1019 100644 --- a/src/Config.h +++ b/src/Config.h @@ -653,9 +653,6 @@ #define ECC_LGFX_EXT_CONF "ext_confs/Lilygo-S3-T-Deck.hpp" - - - #define HAS_TOUCH #define TOUCH_INT 16 #define TOUCH_SDA 18 @@ -710,6 +707,30 @@ // #define TDECK_I2S_DOUT 6 // +#elif defined ARDUINO_SUNTON_4827S043 + + #define ECC_LGFX_EXT_CONF "ext_confs/Sunton-4827S043.hpp" + #define HAS_TOUCH + + #define TFCARD_CS_PIN GPIO_NUM_10 + #define TFCARD_MOSI_PIN GPIO_NUM_11 + #define TFCARD_MISO_PIN GPIO_NUM_13 + #define TFCARD_CLK_PIN GPIO_NUM_12 + + #define SD_ENABLE 1 + #define TFCARD_SPI_FREQ 80000000 + + //#define MONITOR_SERIAL Serial + #define RADAR_SERIAL Serial1 + #define RADAR_RX_PIN GPIO_NUM_17 + #define RADAR_TX_PIN GPIO_NUM_18 + + //#define HAS_BUTTONS + #define BUTTON_A_PIN -1 + #define BUTTON_B_PIN -1 + #define BUTTON_C_PIN -1 + + #define SPEAKER_PIN -1 #else diff --git a/src/ESP32-Chimera-Core.cpp b/src/ESP32-Chimera-Core.cpp index 1545241..e95c37d 100644 --- a/src/ESP32-Chimera-Core.cpp +++ b/src/ESP32-Chimera-Core.cpp @@ -127,7 +127,7 @@ namespace ChimeraCore #if defined HAS_KEYBOARD log_d("Enabling Keyboard"); Keyboard = new Keyboard_Class( - &Wire1, KEYBOARD_I2C_ADDR, KEYBOARD_INT_PIN, + &Wire1, KEYBOARD_SDA_PIN, KEYBOARD_SCL_PIN, KEYBOARD_INT_PIN, KEYBOARD_I2C_ADDR, [](uint8_t key) { [[maybe_unused]]char c[2]={key,0}; log_d("Keyboard Interrupt: char=%s (0x%02x)", c, key); } ); #endif diff --git a/src/drivers/T-Deck/keyboard.h b/src/drivers/T-Deck/keyboard.h index 9fb8ad5..27d4ac0 100644 --- a/src/drivers/T-Deck/keyboard.h +++ b/src/drivers/T-Deck/keyboard.h @@ -26,10 +26,10 @@ namespace ChimeraCore : _bus(bus), _dev_addr(dev_addr), _fire_event(fire_event) { init(); } TDeck_Keyboard_Class( TwoWire* bus, uint8_t dev_addr, gpio_num_t pin_int, fire_event_t fire_event ) : _bus(bus), _pin_int(pin_int), _dev_addr(dev_addr), _fire_event(fire_event) { _has_interrupt = true; init(); } - TDeck_Keyboard_Class( gpio_num_t pin_sda, gpio_num_t pin_scl, uint8_t dev_addr, fire_event_t fire_event ) - : _pin_sda(pin_sda), _pin_scl(pin_scl), _dev_addr(dev_addr), _fire_event(fire_event) { _bus=&Wire1; _has_pins=true; init(); } - TDeck_Keyboard_Class( gpio_num_t pin_sda, gpio_num_t pin_scl, gpio_num_t pin_int, uint8_t dev_addr, fire_event_t fire_event ) - : _pin_sda(pin_sda), _pin_scl(pin_scl), _pin_int(pin_int), _dev_addr(dev_addr), _fire_event(fire_event) { _bus=&Wire; _has_pins=true; _has_interrupt = true; init(); } + TDeck_Keyboard_Class( TwoWire* bus, gpio_num_t pin_sda, gpio_num_t pin_scl, uint8_t dev_addr, fire_event_t fire_event ) + : _bus(bus), _pin_sda(pin_sda), _pin_scl(pin_scl), _dev_addr(dev_addr), _fire_event(fire_event), _has_pins(true) { init(); } + TDeck_Keyboard_Class( TwoWire* bus, gpio_num_t pin_sda, gpio_num_t pin_scl, gpio_num_t pin_int, uint8_t dev_addr, fire_event_t fire_event ) + : _bus(bus), _pin_sda(pin_sda), _pin_scl(pin_scl), _pin_int(pin_int), _dev_addr(dev_addr), _fire_event(fire_event), _has_interrupt(true), _has_pins(true) { init(); } ~TDeck_Keyboard_Class() { @@ -45,17 +45,43 @@ namespace ChimeraCore return false; } if( _has_pins ) { - _bus->begin(_pin_sda, _pin_scl/*, _i2c_freq*/); + _bus->end(); + _bus->begin(_pin_sda, _pin_scl, _i2c_freq); log_d("Starting I2C bus sda=%d,scl=%d", _pin_sda, _pin_scl ); } _bus->requestFrom(_dev_addr, (uint8_t)1); - if (_bus->read() == -1) { + int response = _bus->read(); + if (response == -1) { Serial.println("LILYGO Keyboad not online!"); return false; } + + // uint8_t ret = 0; + // uint8_t reg_helo[4] = { 'h','e','l','o' }; + // _bus->beginTransmission(_dev_addr); // begin + // _bus->write( reg_helo, (uint8_t)4 ); // send HELO message + // ret = _bus->endTransmission(true); // end + // log_d("endTransmission: %u", ret); + // + // // read 4 bytes + // uint8_t bytes_to_read = 4; + // ret = _bus->requestFrom(_dev_addr, bytes_to_read ); // request a total of 16 registers + // + // if( ret == bytes_to_read ) { + // uint8_t resp[bytes_to_read]; + // _bus->readBytes( resp, bytes_to_read ); + // log_print_buf(resp, bytes_to_read); + // // uint16_t b16 = _bus->read() << 8 | _bus->read(); + // // uint8_t b81 = _bus->read(); + // // uint8_t b82 = _bus->read(); + // // log_d("b16: 0x%04x b81: 0x%02x b82: 0x%02x", b16, b81, b82 ); + // } else { + // log_d("Error in requestFrom: %u", ret); + // } + if( _has_interrupt ) { log_d("Attaching interrupt"); - pinMode(_pin_int, INPUT_PULLUP); + pinMode(_pin_int, INPUT/*_PULLUP*/); attachInterrupt(_pin_int, TDeck_Keyboard::ISR_key, FALLING); } return true; @@ -97,22 +123,25 @@ namespace ChimeraCore void update() { if( _has_interrupt ) { - static uint32_t next_key_scan_ms = 0; - if (TDeck_Keyboard::keyboard_interrupted || (millis() > next_key_scan_ms)) { + if (TDeck_Keyboard::keyboard_interrupted ) { + log_d("Interrupt!"); char key; - do { + //do { key = get_key(); if( key !=0 ) { if( _fire_event ) _fire_event( key ); - next_key_scan_ms = millis() + TDeck_Keyboard::key_scan_interval_ms; } - } while( key !=0 ); - TDeck_Keyboard::keyboard_interrupted = false; + //} while( key !=0 ); } + TDeck_Keyboard::keyboard_interrupted = false; } else { - int key = get_key(); - if(key > 0) { - if( _fire_event ) _fire_event( key ); + static uint32_t next_key_scan_ms = 0; + if(millis() > next_key_scan_ms) { + int key = get_key(); + if(key > 0) { + if( _fire_event ) _fire_event( key ); + } + next_key_scan_ms = millis() + TDeck_Keyboard::key_scan_interval_ms; } } } diff --git a/src/drivers/common/Button/Button.h b/src/drivers/common/Button/Button.h index a6dbc97..b4f69ec 100644 --- a/src/drivers/common/Button/Button.h +++ b/src/drivers/common/Button/Button.h @@ -30,6 +30,7 @@ class Button uint8_t pressedFor(uint32_t ms); uint8_t releasedFor(uint32_t ms); uint8_t wasReleasefor(uint32_t ms); + uint8_t wasReleaseFor(uint32_t ms) { return wasReleasefor(ms); } // compliance with M5Unified camel casing uint32_t lastChange(); void setDebounce( uint32_t dbTime_millis) { _dbTime_millis = dbTime_millis; } #ifdef ARDUINO_ODROID_ESP32 diff --git a/src/ext_confs/Lilygo-S3-T-Deck.hpp b/src/ext_confs/Lilygo-S3-T-Deck.hpp index a7e904c..e665bcf 100644 --- a/src/ext_confs/Lilygo-S3-T-Deck.hpp +++ b/src/ext_confs/Lilygo-S3-T-Deck.hpp @@ -88,13 +88,13 @@ cfg.y_max = 320; cfg.bus_shared = true; // I2C - cfg.i2c_port = I2C_NUM_1; + cfg.i2c_port = I2C_NUM_1; // Wire1 cfg.i2c_addr = 0x5D; cfg.pin_sda = GPIO_NUM_18; cfg.pin_scl = GPIO_NUM_8; cfg.pin_int = GPIO_NUM_16; cfg.pin_rst = -1; - cfg.freq = 400000; + cfg.freq = 800000; _touch_instance.config(cfg); _panel_instance.setTouch(&_touch_instance); } diff --git a/src/ext_confs/Sunton-4827S043.hpp b/src/ext_confs/Sunton-4827S043.hpp new file mode 100644 index 0000000..88af610 --- /dev/null +++ b/src/ext_confs/Sunton-4827S043.hpp @@ -0,0 +1,122 @@ +#pragma once + +#include +#include +#include +#include + + + +class LGFX_Makerfabs : public lgfx::LGFX_Device +{ +public: + + lgfx::Bus_RGB _bus_instance; + lgfx::Panel_RGB _panel_instance; + lgfx::Light_PWM _light_instance; + lgfx::Touch_GT911 _touch_instance; + + LGFX_Makerfabs(void) + { + { + auto cfg = _panel_instance.config(); + + cfg.memory_width = 480; + cfg.panel_width = 480; + cfg.memory_height = 272; + cfg.panel_height = 272; + + cfg.offset_x = 0; + cfg.offset_y = 0; + + _panel_instance.config(cfg); + } + + { + auto cfg = _panel_instance.config_detail(); + + cfg.use_psram = 1; + + _panel_instance.config_detail(cfg); + } + + { + auto cfg = _bus_instance.config(); + cfg.panel = &_panel_instance; + + cfg.pin_d0 = GPIO_NUM_8; // B0 + cfg.pin_d1 = GPIO_NUM_3; // B1 + cfg.pin_d2 = GPIO_NUM_46; // B2 + cfg.pin_d3 = GPIO_NUM_9; // B3 + cfg.pin_d4 = GPIO_NUM_1; // B4 + cfg.pin_d5 = GPIO_NUM_5; // G0 + cfg.pin_d6 = GPIO_NUM_6; // G1 + cfg.pin_d7 = GPIO_NUM_7; // G2 + cfg.pin_d8 = GPIO_NUM_15; // G3 + cfg.pin_d9 = GPIO_NUM_16; // G4 + cfg.pin_d10 = GPIO_NUM_4; // G5 + cfg.pin_d11 = GPIO_NUM_45; // R0 + cfg.pin_d12 = GPIO_NUM_48; // R1 + cfg.pin_d13 = GPIO_NUM_47; // R2 + cfg.pin_d14 = GPIO_NUM_21; // R3 + cfg.pin_d15 = GPIO_NUM_14; // R4 + + cfg.pin_henable = GPIO_NUM_40; + cfg.pin_vsync = GPIO_NUM_41; + cfg.pin_hsync = GPIO_NUM_39; + cfg.pin_pclk = GPIO_NUM_42; + cfg.freq_write = 9000000; + + cfg.hsync_polarity = 0; + cfg.hsync_front_porch = 8; + cfg.hsync_pulse_width = 4; + cfg.hsync_back_porch = 43; + cfg.vsync_polarity = 0; + cfg.vsync_front_porch = 8; + cfg.vsync_pulse_width = 4; + cfg.vsync_back_porch = 12; + cfg.pclk_idle_high = 1; + cfg.pclk_active_neg = 1; + _bus_instance.config(cfg); + } + _panel_instance.setBus(&_bus_instance); + + { + auto cfg = _light_instance.config(); + cfg.pin_bl = GPIO_NUM_2; + cfg.invert = true; + _light_instance.config(cfg); + } + _panel_instance.light(&_light_instance); + + { + auto cfg = _touch_instance.config(); + cfg.x_min = 0; + cfg.y_min = 0; + cfg.bus_shared = false; + cfg.offset_rotation = 0; + // I2C接続 + cfg.i2c_port = I2C_NUM_1; + cfg.pin_sda = GPIO_NUM_19; + cfg.pin_scl = GPIO_NUM_20; + cfg.pin_int = GPIO_NUM_NC; + cfg.pin_rst = GPIO_NUM_38; +// #if defined ( LGFX_MAKERFABS_MATOUCH_1024X768 ) +// cfg.x_max = 1024; +// cfg.y_max = 768; +// #else + cfg.x_max = 480; + cfg.y_max = 272; +// #endif + cfg.freq = 400000; + _touch_instance.config(cfg); + _panel_instance.setTouch(&_touch_instance); + } + + setPanel(&_panel_instance); + } +}; + + +#define LGFX LGFX_Makerfabs +using M5Display = LGFX_Makerfabs; diff --git a/src/gitTagVersion.h b/src/gitTagVersion.h index 414dc3c..a1a28af 100644 --- a/src/gitTagVersion.h +++ b/src/gitTagVersion.h @@ -2,7 +2,7 @@ #define ECC_VERSION_MAJOR 1 #define ECC_VERSION_MINOR 5 -#define ECC_VERSION_PATCH 1 +#define ECC_VERSION_PATCH 2 #if !defined _ECC_STR #define _ECC_STR(x) #x #endif diff --git a/src/utility/ScreenShotService/AVI/AVI_Types.hpp b/src/utility/ScreenShotService/AVI/AVI_Types.hpp new file mode 100644 index 0000000..b88f0cc --- /dev/null +++ b/src/utility/ScreenShotService/AVI/AVI_Types.hpp @@ -0,0 +1,102 @@ +#pragma once + +#include +#include + +class AviMjpegEncoder; + +#if defined __JPEGENC__ || __has_include() + #include // https://github.com/bitbank2/JPEGENC + #define HAS_JPEGENC + + // for direct to disk (no jpeg buffer) avi encoding + struct JPGENC_AVI_Proxy_t + { + static AviMjpegEncoder* aviEncoder; + static void* open(const char *filename); + static int32_t write(JPEGFILE *p, uint8_t *buffer, int32_t length); + static void close(JPEGFILE *p); + static int32_t read(JPEGFILE *p, uint8_t *buffer, int32_t length); + static int32_t seek(JPEGFILE *p, int32_t position); + }; + +#endif + +// frame dimensions +struct avi_size_t +{ + uint32_t h, w; +}; + +// index items +struct avi_idx_item_t +{ + uint32_t pos, len; +}; + +// framebuffer to avi stream params model +struct AVI_Params_t +{ + AVI_Params_t( fs::FS* fs, const char* fpath=nullptr, size_t fps=24, bool use_buffer=true, bool use_index_file=true ) + : fs(fs), fps(fps), use_buffer(use_buffer), use_index_file(use_index_file) + { + if( fs ) ready=true; + if( fpath ) path=String(fpath); + } + fs::FS* fs{nullptr}; + String path{""}; + avi_size_t size{0,0}; + size_t fps{24}; + bool use_buffer{true}; + bool use_index_file{true}; + bool ready{false}; +}; + + +// shamelessly overload espressif namespace +namespace fs +{ + // fs::File base implementation extended with writeDword(), writeWord() and writeWordStr() + class AviFile : public fs::FileImpl + { + private: + mutable fs::File _file; + public: + AviFile(fs::File file=FileImplPtr()) : _file(file) {} + virtual ~AviFile() { } + + virtual size_t write(const uint8_t *buf, size_t size) { return _file.write(buf, size); } + virtual size_t read(uint8_t* buf, size_t size) { return _file.read(buf, size); } + virtual void flush() { return _file.flush(); } + virtual size_t position() const { return _file.position(); } + virtual size_t size() const { return _file.size(); } + virtual void close() { _file.close(); } + virtual operator bool() { return _file.operator bool(); } + virtual bool isDirectory(void) { return _file.isDirectory(); } + virtual fs::FileImplPtr openNextFile(const char* mode) { return std::make_shared(_file.openNextFile(mode)); } + virtual bool seekDir(long position) { return _file.seek(position); } + virtual bool seek(uint32_t pos, fs::SeekMode mode=SeekSet) { return _file.seek( pos, mode ); } + virtual const char* name() const { return _file.name(); } + virtual const char* path() const { + #if defined ESP_IDF_VERSION_MAJOR && ESP_IDF_VERSION_MAJOR >= 4 + return _file.path(); + #else + return _file.name(); + #endif + } + virtual String getNextFileName(void) { /* not implemented and not needed */ return String("Unimplemented"); } + virtual String getNextFileName(bool*) { /* not implemented and not needed */ return String("Unimplemented"); } + virtual time_t getLastWrite() { /* not implemented and not needed */ return 0; } + virtual bool setBufferSize(size_t size) { /* not implemented and not needed */ return false; } + virtual void rewindDirectory(void) { /* not implemented and not needed */ } + + virtual size_t readBytes( char* buf, size_t length ) { return _file.readBytes( buf, length ); } + virtual size_t write(const uint8_t byte) { return _file.write(byte); } + virtual uint8_t read() { return _file.read(); } + virtual bool available() { return _file.available(); } + + virtual size_t writeDword(uint32_t word) { return _file.write((const uint8_t *)&word, 4); } + virtual size_t writeWord(uint16_t word) { return _file.write((const uint8_t *)&word, 2); } + virtual size_t writeWordStr(const char* word) { return _file.write((const uint8_t *)word, 4); } + }; +}; diff --git a/src/utility/ScreenShotService/AVI/AviMjpegEncoder.cpp b/src/utility/ScreenShotService/AVI/AviMjpegEncoder.cpp new file mode 100644 index 0000000..d579847 --- /dev/null +++ b/src/utility/ScreenShotService/AVI/AviMjpegEncoder.cpp @@ -0,0 +1,327 @@ + +#include "esp32-hal-log.h" +#include "AviMjpegEncoder.hpp" + +#if defined HAS_JPEGENC + + AviMjpegEncoder* JPGENC_AVI_Proxy_t::aviEncoder = nullptr; + + + void* JPGENC_AVI_Proxy_t::open(const char *filename) + { + assert( JPGENC_AVI_Proxy_t::aviEncoder ); + JPGENC_AVI_Proxy_t::aviEncoder->writeJpegFrameHeader(0); // write empty header + return (void *)JPGENC_AVI_Proxy_t::aviEncoder; + } + + + int32_t JPGENC_AVI_Proxy_t::write(JPEGFILE *p, uint8_t *buffer, int32_t length) + { + auto aviEncoder = (AviMjpegEncoder*)p->fHandle; + aviEncoder->writeJpegFrameData(buffer, length); + return length; + } + + + void JPGENC_AVI_Proxy_t::close(JPEGFILE *p) + { + auto aviEncoder = (AviMjpegEncoder*)p->fHandle; + aviEncoder->writeJpegFrameFinish(); + } + + + int32_t JPGENC_AVI_Proxy_t::read(JPEGFILE *p, uint8_t *buffer, int32_t length) + { + auto aviEncoder = (AviMjpegEncoder*)p->fHandle; + return aviEncoder->aviFile.readBytes((char*)buffer, length); + } + + + int32_t JPGENC_AVI_Proxy_t::seek(JPEGFILE *p, int32_t position) + { + auto aviEncoder = (AviMjpegEncoder*)p->fHandle; + return aviEncoder->aviFile.seek(position); + } + +#endif + + + +AviMjpegEncoder::AviMjpegEncoder( AVI_Params_t *params ) : params( params ) +{ + assert( params ); + assert( params->fs ); + assert( params->path ); + assert( params->size.w>0 && params->size.h>0 ); + + params->ready = false; + aviFile = params->fs->open( params->path, "w" ); + + if( ! aviFile ) { + log_e("Unable to open AVI file %s for writing", params->path.c_str() ); + return; + } + log_d("Opened AVI file %s for writing", params->path.c_str() ); + + if( params->use_index_file ) { + idxFile = params->fs->open(tmpIdxPath, "w"); + if( ! idxFile ) { + log_e("Unable to open IDX file %s for writing", tmpIdxPath ); + return; + } + log_d("Opened IDX file %s for writing", tmpIdxPath ); + } + + writeAviHeader(); + params->ready = true; + log_d("AviMjpegEncoder is ready to encode"); +} + + + +void AviMjpegEncoder::writeAviHeader() +{ + framesCountFieldPos1 = framesCountFieldPos2 = moviPos = jpegFramePos = jpegFrameSize = idx_items_count = 0; + lengthFields.clear(); + imgIndex.clear(); + + // Note: indentation reflects AVI hierarchy + + // Write AVI header + aviFile.writeWordStr("RIFF"); // RIFF type + writeLengthField("RIFF"); // File length (remaining bytes after this field) (nesting level 0) + aviFile.writeWordStr("AVI "); // AVI signature + aviFile.writeWordStr("LIST"); // LIST chunk: data encoding + writeLengthField("hdrl"); // Chunk length (nesting level 1) + aviFile.writeWordStr("hdrl"); // LIST chunk type + aviFile.writeWordStr("avih"); // avih sub-chunk + aviFile.writeDword(56); // Sub-chunk length excluding the first 8 bytes of avih signature and size + aviFile.writeDword(1000000/params->fps); // Frame delay time in microsec + aviFile.writeDword(7000); // dwMaxBytesPerSec (maximum data rate of the file in bytes per second) + aviFile.writeDword(0); // Reserved + aviFile.writeDword(0x10); // dwFlags, 0x10 bit: AVIF_HASINDEX (the AVI file has an index chunk at the end of the file - for good performance); + // Windows Media Player can't even play it if index is missing! + framesCountFieldPos1 = aviFile.position(); + aviFile.writeDword(0); // Number of frames + aviFile.writeDword(0); // Initial frame for non-interleaved files; non interleaved files should set this to 0 + aviFile.writeDword(1); // Number of streams in the video; here 1 video, no audio + aviFile.writeDword(0); // dwSuggestedBufferSize + aviFile.writeDword(params->size.w); // Image width in pixels + aviFile.writeDword(params->size.h); // Image height in pixels + aviFile.writeDword(0); // Reserved + aviFile.writeDword(0); + aviFile.writeDword(0); + aviFile.writeDword(0); + // Write stream information + aviFile.writeWordStr("LIST"); // LIST chunk: stream headers + writeLengthField("strl"); // Chunk size (nesting level 2) + aviFile.writeWordStr("strl"); // LIST chunk type: stream list + aviFile.writeWordStr("strh"); // Stream header // STRH Primitive Chunk (First child of STRL main Header Chunk) + aviFile.writeDword(56); // fixed Length of the strh sub-chunk + aviFile.writeWordStr("vids"); // fccType - type of data stream - here 'vids' for video stream + aviFile.writeWordStr("MJPG"); // MJPG for Motion JPEG + aviFile.writeDword(0); // dwFlags + aviFile.writeDword(0); // wPriority, wLanguage + aviFile.writeDword(0); // dwInitialFrames + aviFile.writeDword(1); // dwScale + aviFile.writeDword(params->fps); // dwRate, Frame rate for video streams (the actual FPS is calculated by dividing this by dwScale) + aviFile.writeDword(0); // usually zero + framesCountFieldPos2 = aviFile.position(); + aviFile.writeDword(0); // dwLength, playing time of AVI file as defined by scale and rate (set equal to the number of frames) + aviFile.writeDword(0); // dwSuggestedBufferSize for reading the stream (typically, this contains a value corresponding to the largest chunk in a stream) + aviFile.writeDword(-1); // dwQuality, encoding quality given by an integer between (0 and 10,000. If set to -1, drivers use the default quality value) + aviFile.writeDword(0); // dwSampleSize, 0 means that each frame is in its own chunk + aviFile.writeWord(0); // left of rcFrame if stream has a different size than dwWidth*dwHeight(unused) + aviFile.writeWord(0); // ..top + aviFile.writeWord(0); // ..right + aviFile.writeWord(0) ; // ..bottom + // end of 'strh' chunk, stream format follows + aviFile.writeWordStr("strf"); // stream format chunk // STRF Primitive Chunk (Second child of STRL main Header Chunk located right after STRH) + writeLengthField("strf"); // Chunk size (nesting level 3) + aviFile.writeDword(40); // biSize, write header size of BITMAPINFO header structure; applications should use this size to + // determine which BITMAPINFO header structure is being used, this size includes this biSize field + aviFile.writeDword(params->size.w);// biWidth, width in pixels + aviFile.writeDword(params->size.h);// biWidth, height in pixels (may be negative for uncompressed video to indicate vertical flip) + aviFile.writeWord(1); // biPlanes, number of color planes in which the data is stored + aviFile.writeWord(24); // biBitCount, number of bits per pixel # + aviFile.writeWordStr("MJPG"); // biCompression, type of compression used (uncompressed: NO_COMPRESSION=0) + aviFile.writeDword(params->size.w*params->size.h*3); // biSizeImage (buffer size for decompressed mage) may be 0 for uncompressed data + aviFile.writeDword(0); // biXPelsPerMeter, horizontal resolution in pixels per meter + aviFile.writeDword(0); // biYPelsPerMeter, vertical resolution in pixels per meter + aviFile.writeDword(0); // biClrUsed (color table size; for 8-bit only) + aviFile.writeDword(0); // biClrImportant, specifies that the first x colors of the color table (0: all colors are important, or, + // rather, their relative importance has not been computed) + finalizeLengthField("strf"); //'strf' chunk finished (nesting level 3) + + aviFile.writeWordStr("strn"); // Use 'strn' to provide a zero terminated text string describing the stream + + auto descString = String( descStr ); + auto fieldlen = descString.length(); // count chars, excluding the \0 terminator + + if( fieldlen%2 == 0 ) { // Name must be 0-terminated and stream name length (the length of the chunk) must be even + descString += " "; // append space if necessary + fieldlen+=2; + } else { + fieldlen++; + } + aviFile.writeDword( fieldlen ); // Length of the strn sub-CHUNK, without the trailing \0 (must be even) + aviFile.write( (const uint8_t*)descString.c_str(), fieldlen ); + if( aviFile.position()%2==1 ) aviFile.write((uint8_t)0x0); // terminating byte + finalizeLengthField("strl"); // LIST 'strl' finished (nesting level 2) + finalizeLengthField("hdrl"); // LIST 'hdrl' finished (nesting level 1) + aviFile.writeWordStr("LIST"); // The second LIST chunk, which contains the actual data + writeLengthField("movi"); // Chunk length (nesting level 1) + moviPos = aviFile.position(); + aviFile.writeWordStr("movi"); // LIST chunk type: 'movi' +} + + +void AviMjpegEncoder::writeLengthField( const char* msg ) +{ + auto pos = aviFile.position(); + lengthFields.push_back( pos ); + aviFile.writeDword(0); + if( msg ) { + log_v("%s.dwSize is at offset %d", msg, pos ); + } +} + + +void AviMjpegEncoder::finalizeLengthField( const char* msg ) +{ + if( lengthFields.size() == 0 ) { + log_e("Error, improper state"); + params->ready = false; + return; + } + auto initial_pos = aviFile.position(); + auto seek_pos = lengthFields[lengthFields.size()-1]; + lengthFields.pop_back(); + assert( initial_pos > seek_pos ); + uint32_t block_size = (initial_pos - seek_pos) - 4; // 4 = sizeof(avi_dword_t); + aviFile.seek( seek_pos ); + aviFile.writeDword( block_size ); + aviFile.seek(initial_pos); +} + + +// add jpeg frame with known length +void AviMjpegEncoder::addJpegFrame(const uint8_t *jpegData, int len) +{ + writeJpegFrameHeader( len ); + writeJpegFrameData( jpegData, len ); + writeJpegFrameFinish(); +} + + +// add AVI header for jpeg frame +void AviMjpegEncoder::writeJpegFrameHeader(int len) +{ + jpegFrameSize = 0; + aviFile.writeWordStr("00dc"); + if( len == 0 ) { + writeLengthField("00dc"); + needs_finalize = true; + } else { // len provided, no need to populate later + aviFile.writeDword(len); + needs_finalize = false; + } + jpegFramePos = aviFile.position(); +} + + +// add jpeg frame data chunk (repeated calls) +void AviMjpegEncoder::writeJpegFrameData(const uint8_t *jpegData, int len) +{ + jpegFrameSize += len; + aviFile.write( jpegData, len ); +} + + +// add AVI footer for jpeg frame +void AviMjpegEncoder::writeJpegFrameFinish() +{ + if( jpegFrameSize%2 ) { // jpeg length is odd + aviFile.write((uint8_t)0x0); // even the length + jpegFrameSize++; + } + if( needs_finalize ) { + finalizeLengthField("00dc"); // "00dc" chunk finished (nesting level 2) + } + if( params->use_index_file ) { // append to idx file + writeJpegIdxItem( &idxFile, { jpegFramePos, jpegFrameSize } ); + idx_items_count++; + } else { // store in vector + imgIndex.push_back( { jpegFramePos, jpegFrameSize } ); + } + log_v("[%s] Added frame off=%d len=%d", params->use_index_file?"idx":"mem", jpegFramePos, jpegFrameSize ); +} + + +void AviMjpegEncoder::writeJpegIdxItem( fs::AviFile* dst, avi_idx_item_t item ) +{ + assert( dst ); + dst->writeWordStr("00dc"); + dst->writeDword(0x10); // flags: select AVIIF_KEYFRAME + dst->writeDword(item.pos); // offset to the chunk, offset can be relative to file start or 'movi' + dst->writeDword(item.len); // length of the chunk +} + + + +void AviMjpegEncoder::finalize() +{ + log_d("Finalizing"); + + uint32_t framesCount = params->use_index_file ? idx_items_count : imgIndex.size(); + uint32_t idxLength = framesCount*16; + uint8_t frameInfo[16]; + avi_idx_item_t *item; + + finalizeLengthField("movi"); // LIST 'movi' finished (nesting level 1) + // Write index + aviFile.writeWordStr("idx1"); // idx1 chunk + aviFile.writeDword(idxLength); // Chunk length + + if( params->use_index_file ) { + idxFile.close(); + idxFile = params->fs->open(tmpIdxPath); // reopen as readonly + if( !idxFile ) { + log_e("Unable to reopen %s index file for reading, aborting", tmpIdxPath); + params->ready = false; + return; + } + } + + for( int i=0;iuse_index_file ) { + if( idxFile.readBytes( (char*)frameInfo, 16 ) != 16 ) { + log_e("Unexpected EOF in index file at offset %d (idxLength=%d), aborting", i, idxLength ); + params->ready = false; + return; + } + item = (avi_idx_item_t *)&frameInfo[8]; + } else { + item = &imgIndex[i]; + } + writeJpegIdxItem( &aviFile, { item->pos, item->len } ); + } + + if( params->use_index_file ) { + idxFile.close(); + params->fs->remove(tmpIdxPath); + } + + auto pos = aviFile.position(); + aviFile.seek(framesCountFieldPos1); + aviFile.writeDword(framesCount); + aviFile.seek(framesCountFieldPos2); + aviFile.writeDword(framesCount); + aviFile.seek(pos); + finalizeLengthField("RIFF"); // 'RIFF' finished (nesting level 0) + + aviFile.close(); + + imgIndex.clear(); + lengthFields.clear(); +} diff --git a/src/utility/ScreenShotService/AVI/AviMjpegEncoder.hpp b/src/utility/ScreenShotService/AVI/AviMjpegEncoder.hpp new file mode 100644 index 0000000..9b85ffe --- /dev/null +++ b/src/utility/ScreenShotService/AVI/AviMjpegEncoder.hpp @@ -0,0 +1,50 @@ +#pragma once + +#include +#include "AVI_Types.hpp" + +class AviMjpegEncoder +{ + public: + + AviMjpegEncoder( AVI_Params_t *params ); + + // add a whole jpeg frame + void addJpegFrame(const uint8_t *jpegData, int len); + // add jpeg frame by chunks + void writeJpegFrameHeader(int len=0); + void writeJpegFrameData(const uint8_t *jpegData, int len); + void writeJpegFrameFinish(); + + void finalize(); + + size_t framesCount() { return imgIndex.size(); } + + fs::AviFile aviFile; + fs::AviFile idxFile; + + private: + + AVI_Params_t *params; + + // interesting offsets in the aviFile when finalizing + uint32_t framesCountFieldPos1, framesCountFieldPos2, moviPos, jpegFramePos, jpegFrameSize; + + void writeAviHeader(); // Note: called from constructor + void writeFrameHeader(int len); + void writeFrame(const uint8_t *data, int len); + void writeLengthField(const char* msg = nullptr); + void finalizeLengthField(const char* msg = nullptr); + std::vector lengthFields; + + uint32_t idx_items_count = 0; + void writeJpegIdxItem( fs::AviFile* dst, avi_idx_item_t item ); + std::vectorimgIndex; + bool needs_finalize; + + const char* tmpIdxPath = "/tmp.idx"; + const char* descStr = "This video was created with ESP32-AVIEncoder - copyleft (c+) tobozo 2023"; + +}; + + diff --git a/src/utility/ScreenShotService/JPG/TinyJPEGEncoder.cpp b/src/utility/ScreenShotService/JPG/TinyJPEGEncoder.cpp deleted file mode 100644 index 4b16f4a..0000000 --- a/src/utility/ScreenShotService/JPG/TinyJPEGEncoder.cpp +++ /dev/null @@ -1,908 +0,0 @@ -/* - * - * TinyJPEGEncoder - * - * Copyright 2019 tobozo http://github.com/tobozo - * - * Permission is hereby granted, free of charge, to any person - * obtaining a copy of this software and associated documentation - * files ("TinyBMPEncoder"), to deal in the Software without - * restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following - * conditions: - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - * OTHER DEALINGS IN THE SOFTWARE. - * - * This is a readable and simple single-header JPEG encoder. - * - * Features - * - Implements Baseline DCT JPEG compression. - * - No dynamic allocations. - * - Writes to SD/SD_MMC/SPIFFS - * - * ==== Thanks ==== - * - * AssociationSirius (Bug reports) - * Bernard van Gastel (Thread-safe defaults, BSD compilation) - * Sergio Gonzalez (initial C/std library https://github.com/serge-rgb/TinyJPEG) - * - * ==== License ==== - * - * This software is in the public domain. Where that dedication is not - * recognized, you are granted a perpetual, irrevocable license to copy and - * modify this file as you see fit. - * - * === CHANGES by tobozo Nov. 2019=== - * - * - uses psram if available - * - uses fs::FS - * - rewrotecode to in a C++ style - * - */ - -// why would espressif suddenly make these behave like errors? -#pragma GCC diagnostic ignored "-Wmissing-field-initializers" -#pragma GCC diagnostic ignored "-Wimplicit-fallthrough" - - -#include "TinyJPEGEncoder.hpp" -// ============================================================ -// Table definitions. -// -// The spec defines tjei_default reasonably good quantization matrices and huffman -// specification tables. -// -// -// Instead of hard-coding the final huffman table, we only hard-code the table -// spec suggested by the specification, and then derive the full table from -// there. This is only for didactic purposes but it might be useful if there -// ever is the case that we need to swap huffman tables from various sources. -// ============================================================ - - -// K.1 - suggested luminance QT -static const uint8_t tjei_default_qt_luma_from_spec[] = { - 16,11,10,16, 24, 40, 51, 61, - 12,12,14,19, 26, 58, 60, 55, - 14,13,16,24, 40, 57, 69, 56, - 14,17,22,29, 51, 87, 80, 62, - 18,22,37,56, 68,109,103, 77, - 24,35,55,64, 81,104,113, 92, - 49,64,78,87,103,121,120,101, - 72,92,95,98,112,100,103, 99, -}; - -// Unused -#if 0 -static const uint8_t tjei_default_qt_chroma_from_spec[] = { - // K.1 - suggested chrominance QT - 17,18,24,47,99,99,99,99, - 18,21,26,66,99,99,99,99, - 24,26,56,99,99,99,99,99, - 47,66,99,99,99,99,99,99, - 99,99,99,99,99,99,99,99, - 99,99,99,99,99,99,99,99, - 99,99,99,99,99,99,99,99, - 99,99,99,99,99,99,99,99, -}; -#endif - -static const uint8_t tjei_default_qt_chroma_from_paper[] = { - // Example QT from JPEG paper - 16, 12, 14, 14, 18, 24, 49, 72, - 11, 10, 16, 24, 40, 51, 61, 12, - 13, 17, 22, 35, 64, 92, 14, 16, - 22, 37, 55, 78, 95, 19, 24, 29, - 56, 64, 87, 98, 26, 40, 51, 68, - 81, 103, 112, 58, 57, 87, 109, 104, - 121,100, 60, 69, 80, 103, 113, 120, - 103, 55, 56, 62, 77, 92, 101, 99, -}; - -// == Procedure to 'deflate' the huffman tree: JPEG spec, C.2 - -// Number of 16 bit values for every code length. (K.3.3.1) -static const uint8_t tjei_default_ht_luma_dc_len[16] = { 0,1,5,1,1,1,1,1,1,0,0,0,0,0,0,0 }; -// values -static const uint8_t tjei_default_ht_luma_dc[12] = { 0,1,2,3,4,5,6,7,8,9,10,11 }; - -// Number of 16 bit values for every code length. (K.3.3.1) -static const uint8_t tjei_default_ht_chroma_dc_len[16] = { 0,3,1,1,1,1,1,1,1,1,1,0,0,0,0,0 }; -// values -static const uint8_t tjei_default_ht_chroma_dc[12] = { 0,1,2,3,4,5,6,7,8,9,10,11 }; - -// Same as above, but AC coefficients. -static const uint8_t tjei_default_ht_luma_ac_len[16] = { 0,2,1,3,3,2,4,3,5,5,4,4,0,0,1,0x7d }; -static const uint8_t tjei_default_ht_luma_ac[] = { - 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21, 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07, - 0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xA1, 0x08, 0x23, 0x42, 0xB1, 0xC1, 0x15, 0x52, 0xD1, 0xF0, - 0x24, 0x33, 0x62, 0x72, 0x82, 0x09, 0x0A, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x25, 0x26, 0x27, 0x28, - 0x29, 0x2A, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, - 0x4A, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, - 0x6A, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, - 0x8A, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, - 0xA8, 0xA9, 0xAA, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, 0xC2, 0xC3, 0xC4, 0xC5, - 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xE1, 0xE2, - 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, - 0xF9, 0xFA -}; - -static const uint8_t tjei_default_ht_chroma_ac_len[16] = { 0,2,1,2,4,4,3,4,7,5,4,4,0,1,2,0x77 }; -static const uint8_t tjei_default_ht_chroma_ac[] = { - 0x00, 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21, 0x31, 0x06, 0x12, 0x41, 0x51, 0x07, 0x61, 0x71, - 0x13, 0x22, 0x32, 0x81, 0x08, 0x14, 0x42, 0x91, 0xA1, 0xB1, 0xC1, 0x09, 0x23, 0x33, 0x52, 0xF0, - 0x15, 0x62, 0x72, 0xD1, 0x0A, 0x16, 0x24, 0x34, 0xE1, 0x25, 0xF1, 0x17, 0x18, 0x19, 0x1A, 0x26, - 0x27, 0x28, 0x29, 0x2A, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, - 0x49, 0x4A, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, - 0x69, 0x6A, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, - 0x88, 0x89, 0x8A, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0xA2, 0xA3, 0xA4, 0xA5, - 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, 0xC2, 0xC3, - 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, - 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, - 0xF9, 0xFA -}; - - -// ============================================================ -// Code -// ============================================================ - -// Zig-zag order: -static const uint8_t tjei_zig_zag[64] = { - 0, 1, 5, 6, 14, 15, 27, 28, - 2, 4, 7, 13, 16, 26, 29, 42, - 3, 8, 12, 17, 25, 30, 41, 43, - 9, 11, 18, 24, 31, 40, 44, 53, - 10, 19, 23, 32, 39, 45, 52, 54, - 20, 22, 33, 38, 46, 51, 55, 60, - 21, 34, 37, 47, 50, 56, 59, 61, - 35, 36, 48, 49, 57, 58, 62, 63, -}; - - - -// ============================================================ -// Internal -// ============================================================ - -#define tjei_min(a, b) ((a) < b) ? (a) : (b); -#define tjei_max(a, b) ((a) < b) ? (b) : (a); - -// Memory order as big endian. 0xhilo -> 0xlohi which looks as 0xhilo in memory. -static uint16_t tjei_be_word( const uint16_t le_word ) { - uint16_t lo = ( le_word & 0x00ff ); - uint16_t hi = ( ( le_word & 0xff00 ) >> 8 ); - return (uint16_t)( ( lo << 8 ) | hi ); -} - - - - -void JPEG_Encoder::begin( bool ifPsram ) { - if( ifPsram && psramInit() ) { - huffsize = (uint8_t**)ps_calloc(4*257, sizeof(uint8_t)); - for(byte i=0; i<4; i++) { - huffsize[i] = (uint8_t*)ps_calloc(257, sizeof(uint8_t) ); - } - huffcode = (uint16_t**)ps_calloc(4*256, sizeof(uint16_t)); - for(byte i=0; i<4; i++) { - huffcode[i] = (uint16_t*)ps_calloc(256, sizeof(uint16_t) ); - } - } else { - huffsize = (uint8_t**)calloc(4*257, sizeof(uint8_t)); - for(byte i=0; i<4; i++) { - huffsize[i] = (uint8_t*)calloc(257, sizeof(uint8_t) ); - } - huffcode = (uint16_t**)calloc(4*256, sizeof(uint16_t)); - for(byte i=0; i<4; i++) { - huffcode[i] = (uint16_t*)calloc(256, sizeof(uint16_t) ); - } - } -} - -// Usage: -// Takes bitmap data and writes a JPEG-encoded image to disk. -// -// PARAMETERS -// dest_path: filename to which we will write. e.g. "out.jpg" -// width, height: image size in pixels -// numComponents: 3 is RGB. 4 is RGBA. Those are the only supported values -// src_data: pointer to the pixel data. -// -// RETURN: -// 0 on error. 1 on success. -int JPEG_Encoder::encodeToFile( const char* dest_path, const int width, const int height, const int numComponents, unsigned char* src_data, jpeg_encoder_callback_t fp, void* device ) { - int res = encodeToFileAtQuality( dest_path, 3/*2*/, width, height, numComponents, src_data, fp, device ); - return res; -} - - - - -// Usage -// Same as encodeToFileAtQuality, but it takes a callback that knows -// how to handle (or ignore) `context`. The callback receives an array `data` -// of `size` bytes, which can be written directly to a file. There is no need -// to free the data. -int JPEG_Encoder::encodeWithFunc( writeFunc* func, fs::File jpegFile, const int quality, const int width, const int height, const int numComponents, unsigned char* src_data, jpeg_encoder_callback_t fp, void* device ) { - if ( quality < 1 || quality > 3 ) { - tje_log( "[ERROR] -- Valid 'quality' values are 1 (lowest), 2, or 3 (highest)\n" ); - return 0; - } - TJEState state = { 0 }; - uint8_t qt_factor = 1; - switch( quality ) { - case 3: - for ( int i = 0; i < 64; ++i ) { - state.qt_luma[i] = 1; - state.qt_chroma[i] = 1; - } - break; - case 2: - qt_factor = 10; - // don't break. fall through. - case 1: - for ( int i = 0; i < 64; ++i ) { - state.qt_luma[i] = tjei_default_qt_luma_from_spec[i] / qt_factor; - if ( state.qt_luma[i] == 0 ) { - state.qt_luma[i] = 1; - } - state.qt_chroma[i] = tjei_default_qt_chroma_from_paper[i] / qt_factor; - if ( state.qt_chroma[i] == 0 ) { - state.qt_chroma[i] = 1; - } - } - break; - default: - assert( !"invalid code path" ); - break; - } - TJEWriteContext wc = { jpegFile, 0 }; - wc.func = func; - state.writeContext = wc; - huffExpand( &state ); - int result = encodeMain( &state, src_data, fp, device, width, height, numComponents ); - return result; -} - - - - - -void JPEG_Encoder::write( TJEState* state, const void* data, size_t num_bytes, size_t num_elements ) { - size_t to_write = num_bytes * num_elements; - // Cap to the buffer available size and copy memory. - size_t capped_count = tjei_min( to_write, TJEI_BUFFER_SIZE - 1 - state->outputBufferCount ); - memcpy( state->outputBuffer + state->outputBufferCount, data, capped_count ); - state->outputBufferCount += capped_count; - assert ( state->outputBufferCount <= TJEI_BUFFER_SIZE - 1 ); - // Flush the buffer. - if ( state->outputBufferCount == TJEI_BUFFER_SIZE - 1 ) { - state->writeContext.func( state->writeContext.jpegFile, state->outputBuffer, (int)state->outputBufferCount ); - state->outputBufferCount = 0; - } - // Recursively calling ourselves with the rest of the buffer. - if ( capped_count < to_write ) { - write( state, (uint8_t*)data+capped_count, to_write - capped_count, 1 ); - } -} - - -void JPEG_Encoder::write_DQT( TJEState* state, const uint8_t* matrix, uint8_t id ) { - uint16_t DQT = tjei_be_word( 0xffdb ); - write( state, &DQT, sizeof(uint16_t), 1 ); - uint16_t len = tjei_be_word( 0x0043 ); // 2(len) + 1(id) + 64(matrix) = 67 = 0x43 - write( state, &len, sizeof(uint16_t), 1 ); - assert( id < 4 ); - uint8_t precision_and_id = id; // 0x0000 8 bits | 0x00id - write( state, &precision_and_id, sizeof(uint8_t), 1 ); - // Write matrix - write( state, matrix, 64*sizeof(uint8_t), 1 ); -} - - -void JPEG_Encoder::write_DHT( TJEState* state, uint8_t const * matrix_len, uint8_t const * matrix_val, TJEHuffmanTableClass ht_class, uint8_t id ) { - int num_values = 0; - for ( int i = 0; i < 16; ++i ) { - num_values += matrix_len[i]; - } - assert( num_values <= 0xffff ); - uint16_t DHT = tjei_be_word( 0xffc4 ); - // 2(len) + 1(Tc|th) + 16 (num lengths) + ?? (num values) - uint16_t len = tjei_be_word( 2 + 1 + 16 + (uint16_t)num_values ); - assert( id < 4 ); - uint8_t tc_th = (uint8_t)( ( ( (uint8_t)ht_class ) << 4 ) | id ); - write( state, &DHT, sizeof(uint16_t), 1 ); - write( state, &len, sizeof(uint16_t), 1 ); - write( state, &tc_th, sizeof(uint8_t), 1 ); - write( state, matrix_len, sizeof(uint8_t), 16 ); - write( state, matrix_val, sizeof(uint8_t), (size_t)num_values ); -} - -// ============================================================ -// Huffman deflation code. -// ============================================================ -// Returns all code sizes from the BITS specification (JPEG C.3) -uint8_t* JPEG_Encoder::huffGetCodeLengths( uint8_t huffsize[/*256*/], uint8_t const * bits ) { - int k = 0; - for ( int i = 0; i < 16; ++i ) { - for ( int j = 0; j < bits[i]; ++j ) { - huffsize[k++] = (uint8_t)(i + 1); - } - huffsize[k] = 0; - } - return huffsize; -} - - -// Fills out the prefixes for each code. -uint16_t* JPEG_Encoder::huffGetCodes( uint16_t codes[], uint8_t* huffsize, int64_t count ) { - uint16_t code = 0; - int k = 0; - uint8_t sz = huffsize[0]; - for(;;) { - do { - assert(k < count); - codes[k++] = code++; - } while ( huffsize[k] == sz ); - if (huffsize[k] == 0) { - return codes; - } - do { - code = (uint16_t)( code << 1 ); - ++sz; - } while( huffsize[k] != sz ); - } -} - - -void JPEG_Encoder::huffGetExtended( uint8_t* out_ehuffsize, uint16_t* out_ehuffcode, uint8_t const * huffval, uint8_t* huffsize, uint16_t* huffcode, int64_t count ) { - int k = 0; - do { - uint8_t val = huffval[k]; - out_ehuffcode[val] = huffcode[k]; - out_ehuffsize[val] = huffsize[k]; - k++; - } while ( k < count ); -} -// Returns: -// out[1] : number of bits -// out[0] : bits -void JPEG_Encoder::calcVarLengthAsInt( int value, uint16_t out[2] ) { - int abs_val = value; - if ( value < 0 ) { - abs_val = -abs_val; - --value; - } - out[1] = 1; - while( abs_val >>= 1 ) { - ++out[1]; - } - out[0] = (uint16_t)( value & ( ( 1 << out[1] ) - 1 ) ); -} - - -// Write bits to file. -void JPEG_Encoder::writeBits( TJEState* state, uint32_t* bitbuffer, uint32_t* location, uint16_t num_bits, uint16_t bits ) { - // v-- location - // [ ] <-- bit buffer - // 32 0 - // - // This call pushes to the bitbuffer and saves the location. Data is pushed - // from most significant to less significant. - // When we can write a full byte, we write a byte and shift. - // Push the stack. - uint32_t nloc = *location + num_bits; - *bitbuffer |= (uint32_t)( bits << ( 32 - nloc ) ); - *location = nloc; - while ( *location >= 8 ) { - // Grab the most significant byte. - uint8_t c = (uint8_t)( ( *bitbuffer ) >> 24 ); - // Write it to file. - write( state, &c, 1, 1 ); - if ( c == 0xff ) { - // Special case: tell JPEG this is not a marker. - char z = 0; - write( state, &z, 1, 1 ); - } - // Pop the stack. - *bitbuffer <<= 8; - *location -= 8; - } -} - - -// DCT implementation by Thomas G. Lane. -// Obtained through NVIDIA -// http://developer.download.nvidia.com/SDK/9.5/Samples/vidimaging_samples.html#gpgpu_dct -// -// QUOTE: -// This implementation is based on Arai, Agui, and Nakajima's algorithm for -// scaled DCT. Their original paper (Trans. IEICE E-71(11):1095) is in -// Japanese, but the algorithm is described in the Pennebaker & Mitchell -// JPEG textbook (see REFERENCES section in file README). The following code -// is based directly on figure 4-8 in P&M. -// -void JPEG_Encoder::fast_fdct ( float * data ) { - float tmp0, tmp1, tmp2, tmp3, tmp4, tmp5, tmp6, tmp7; - float tmp10, tmp11, tmp12, tmp13; - float z1, z2, z3, z4, z5, z11, z13; - float *dataptr; - int ctr; - /* Pass 1: process rows. */ - dataptr = data; - for ( ctr = 7; ctr >= 0; ctr-- ) { - tmp0 = dataptr[0] + dataptr[7]; - tmp7 = dataptr[0] - dataptr[7]; - tmp1 = dataptr[1] + dataptr[6]; - tmp6 = dataptr[1] - dataptr[6]; - tmp2 = dataptr[2] + dataptr[5]; - tmp5 = dataptr[2] - dataptr[5]; - tmp3 = dataptr[3] + dataptr[4]; - tmp4 = dataptr[3] - dataptr[4]; - /* Even part */ - tmp10 = tmp0 + tmp3; /* phase 2 */ - tmp13 = tmp0 - tmp3; - tmp11 = tmp1 + tmp2; - tmp12 = tmp1 - tmp2; - dataptr[0] = tmp10 + tmp11; /* phase 3 */ - dataptr[4] = tmp10 - tmp11; - z1 = (tmp12 + tmp13) * ((float) 0.707106781); /* c4 */ - dataptr[2] = tmp13 + z1; /* phase 5 */ - dataptr[6] = tmp13 - z1; - /* Odd part */ - tmp10 = tmp4 + tmp5; /* phase 2 */ - tmp11 = tmp5 + tmp6; - tmp12 = tmp6 + tmp7; - /* The rotator is modified from fig 4-8 to avoid extra negations. */ - z5 = (tmp10 - tmp12) * ((float) 0.382683433); /* c6 */ - z2 = ((float) 0.541196100) * tmp10 + z5; /* c2-c6 */ - z4 = ((float) 1.306562965) * tmp12 + z5; /* c2+c6 */ - z3 = tmp11 * ((float) 0.707106781); /* c4 */ - z11 = tmp7 + z3; /* phase 5 */ - z13 = tmp7 - z3; - dataptr[5] = z13 + z2; /* phase 6 */ - dataptr[3] = z13 - z2; - dataptr[1] = z11 + z4; - dataptr[7] = z11 - z4; - dataptr += 8; /* advance pointer to next row */ - } - /* Pass 2: process columns. */ - dataptr = data; - for ( ctr = 8-1; ctr >= 0; ctr-- ) { - tmp0 = dataptr[8*0] + dataptr[8*7]; - tmp7 = dataptr[8*0] - dataptr[8*7]; - tmp1 = dataptr[8*1] + dataptr[8*6]; - tmp6 = dataptr[8*1] - dataptr[8*6]; - tmp2 = dataptr[8*2] + dataptr[8*5]; - tmp5 = dataptr[8*2] - dataptr[8*5]; - tmp3 = dataptr[8*3] + dataptr[8*4]; - tmp4 = dataptr[8*3] - dataptr[8*4]; - /* Even part */ - tmp10 = tmp0 + tmp3; /* phase 2 */ - tmp13 = tmp0 - tmp3; - tmp11 = tmp1 + tmp2; - tmp12 = tmp1 - tmp2; - dataptr[8*0] = tmp10 + tmp11; /* phase 3 */ - dataptr[8*4] = tmp10 - tmp11; - z1 = (tmp12 + tmp13) * ((float) 0.707106781); /* c4 */ - dataptr[8*2] = tmp13 + z1; /* phase 5 */ - dataptr[8*6] = tmp13 - z1; - /* Odd part */ - tmp10 = tmp4 + tmp5; /* phase 2 */ - tmp11 = tmp5 + tmp6; - tmp12 = tmp6 + tmp7; - /* The rotator is modified from fig 4-8 to avoid extra negations. */ - z5 = (tmp10 - tmp12) * ((float) 0.382683433); /* c6 */ - z2 = ((float) 0.541196100) * tmp10 + z5; /* c2-c6 */ - z4 = ((float) 1.306562965) * tmp12 + z5; /* c2+c6 */ - z3 = tmp11 * ((float) 0.707106781); /* c4 */ - z11 = tmp7 + z3; /* phase 5 */ - z13 = tmp7 - z3; - dataptr[8*5] = z13 + z2; /* phase 6 */ - dataptr[8*3] = z13 - z2; - dataptr[8*1] = z11 + z4; - dataptr[8*7] = z11 - z4; - dataptr++; /* advance pointer to next column */ - } -} - -#if !TJE_USE_FAST_DCT - float JPEG_Encoder::slow_fdct( int u, int v, float* data ) { - #define kPI 3.14159265f - float res = 0.0f; - float cu = (u == 0) ? 0.70710678118654f : 1; - float cv = (v == 0) ? 0.70710678118654f : 1; - for ( int y = 0; y < 8; ++y ) { - for ( int x = 0; x < 8; ++x ) { - res += ( data[y * 8 + x] ) * - cosf( ( ( 2.0f * x + 1.0f ) * u * kPI ) / 16.0f ) * - cosf( ( ( 2.0f * y + 1.0f ) * v * kPI ) / 16.0f ); - } - } - res *= 0.25f * cu * cv; - return res; - #undef kPI - } -#endif - -#define ABS(x) ((x) < 0 ? -(x) : (x)) - -void JPEG_Encoder::encodeAndWriteMCU( TJEState* state, - float* mcu, -#if TJE_USE_FAST_DCT - float* qt, // Pre-processed quantization matrix. -#else - uint8_t* qt, -#endif - uint8_t* huff_dc_len, uint16_t* huff_dc_code, // Huffman tables - uint8_t* huff_ac_len, uint16_t* huff_ac_code, - int* pred, // Previous DC coefficient - uint32_t* bitbuffer, // Bitstack. - uint32_t* location ) { - - int du[64]; // Data unit in zig-zag order - float dct_mcu[64]; - memcpy( dct_mcu, mcu, 64 * sizeof(float) ); - -#if TJE_USE_FAST_DCT - fast_fdct(dct_mcu); - for ( int i = 0; i < 64; ++i ) { - float fval = dct_mcu[i]; - fval *= qt[i]; -#if 0 - fval = (fval > 0) ? floorf(fval + 0.5f) : ceilf(fval - 0.5f); -#else - fval = floorf( fval + 1024 + 0.5f ); - fval -= 1024; -#endif - int val = (int)fval; - du[tjei_zig_zag[i]] = val; - } -#else - for ( int v = 0; v < 8; ++v ) { - for ( int u = 0; u < 8; ++u ) { - dct_mcu[v * 8 + u] = slow_fdct( u, v, mcu ); - } - } - for ( int i = 0; i < 64; ++i ) { - float fval = dct_mcu[i] / (qt[i]); - int val = (int)( ( fval > 0 ) ? floorf( fval + 0.5f ) : ceilf( fval - 0.5f ) ); - du[tjei_zig_zag[i]] = val; - } -#endif - - uint16_t vli[2]; - // Encode DC coefficient. - int diff = du[0] - *pred; - *pred = du[0]; - if ( diff != 0 ) { - calcVarLengthAsInt( diff, vli ); - // Write number of bits with Huffman coding - writeBits( state, bitbuffer, location, huff_dc_len[vli[1]], huff_dc_code[vli[1]] ); - // Write the bits. - writeBits( state, bitbuffer, location, vli[1], vli[0] ); - } else { - writeBits( state, bitbuffer, location, huff_dc_len[0], huff_dc_code[0] ); - } - // ==== Encode AC coefficients ==== - int last_non_zero_i = 0; - // Find the last non-zero element. - for ( int i = 63; i > 0; --i ) { - if (du[i] != 0) { - last_non_zero_i = i; - break; - } - } - for ( int i = 1; i <= last_non_zero_i; ++i ) { - // If zero, increase count. If >=15, encode (FF,00) - int zero_count = 0; - while ( du[i] == 0 ) { - ++zero_count; - ++i; - if (zero_count == 16) { - // encode (ff,00) == 0xf0 - writeBits( state, bitbuffer, location, huff_ac_len[0xf0], huff_ac_code[0xf0] ); - zero_count = 0; - } - } - calcVarLengthAsInt( du[i], vli ); - assert( zero_count < 0x10 ); - assert( vli[1] <= 10 ); - uint16_t sym1 = (uint16_t)((uint16_t)zero_count << 4) | vli[1]; - assert( huff_ac_len[sym1] != 0 ); - // Write symbol 1 --- (RUNLENGTH, SIZE) - writeBits( state, bitbuffer, location, huff_ac_len[sym1], huff_ac_code[sym1] ); - // Write symbol 2 --- (AMPLITUDE) - writeBits( state, bitbuffer, location, vli[1], vli[0] ); - } - if (last_non_zero_i != 63) { - // write EOB HUFF(00,00) - writeBits( state, bitbuffer, location, huff_ac_len[0], huff_ac_code[0] ); - } - return; -} - - -// Set up huffman tables in state. -void JPEG_Encoder::huffExpand( TJEState* state ) { - assert( state ); - state->ht_bits[TJEI_LUMA_DC] = tjei_default_ht_luma_dc_len; - state->ht_bits[TJEI_LUMA_AC] = tjei_default_ht_luma_ac_len; - state->ht_bits[TJEI_CHROMA_DC] = tjei_default_ht_chroma_dc_len; - state->ht_bits[TJEI_CHROMA_AC] = tjei_default_ht_chroma_ac_len; - state->ht_vals[TJEI_LUMA_DC] = tjei_default_ht_luma_dc; - state->ht_vals[TJEI_LUMA_AC] = tjei_default_ht_luma_ac; - state->ht_vals[TJEI_CHROMA_DC] = tjei_default_ht_chroma_dc; - state->ht_vals[TJEI_CHROMA_AC] = tjei_default_ht_chroma_ac; - // How many codes in total for each of LUMA_(DC|AC) and CHROMA_(DC|AC) - int32_t spec_tables_len[4] = { 0 }; - for ( int i = 0; i < 4; ++i ) { - for ( int k = 0; k < 16; ++k ) { - spec_tables_len[i] += state->ht_bits[i][k]; - } - } - // Fill out the extended tables.. - for(byte i=0; i<4; i++) { - *huffsize[i] = {'\0'}; - } - for(byte i=0; i<4; i++) { - *huffcode[i] = {'\0'}; - } - for ( int i = 0; i < 4; ++i ) { - assert (256 >= spec_tables_len[i]); - huffGetCodeLengths( huffsize[i], state->ht_bits[i] ); - huffGetCodes( huffcode[i], huffsize[i], spec_tables_len[i] ); - } - for ( int i = 0; i < 4; ++i ) { - int64_t count = spec_tables_len[i]; - huffGetExtended( state->ehuffsize[i], state->ehuffcode[i], state->ht_vals[i], &huffsize[i][0], &huffcode[i][0], count ); - } -} - - -int JPEG_Encoder::encodeMain( TJEState* state, unsigned char* src_data, jpeg_encoder_callback_t fp, void* device, const int width, const int height, const int srcNumComponents ) { - if ( srcNumComponents != 3 && srcNumComponents != 4 ) { - return 0; - } - if ( width > 0xffff || height > 0xffff ) { - return 0; - } - -#if TJE_USE_FAST_DCT - struct TJEProcessedQT pqt; - // Again, taken from classic japanese implementation. - // - /* For float AA&N IDCT method, divisors are equal to quantization - * coefficients scaled by scalefactor[row]*scalefactor[col], where - * scalefactor[0] = 1 - * scalefactor[k] = cos(k*PI/16) * sqrt(2) for k=1..7 - * We apply a further scale factor of 8. - * What's actually stored is 1/divisor so that the inner loop can - * use a multiplication rather than a division. - */ - static const float aan_scales[] = { - 1.0f, 1.387039845f, 1.306562965f, 1.175875602f, - 1.0f, 0.785694958f, 0.541196100f, 0.275899379f - }; - // build (de)quantization tables - for( int y=0; y<8; y++ ) { - for( int x=0; x<8; x++ ) { - int i = y*8 + x; - pqt.luma[y*8+x] = 1.0f / (8 * aan_scales[x] * aan_scales[y] * state->qt_luma[tjei_zig_zag[i]]); - pqt.chroma[y*8+x] = 1.0f / (8 * aan_scales[x] * aan_scales[y] * state->qt_chroma[tjei_zig_zag[i]]); - } - } -#endif - { // Write header - TJEJPEGHeader header; - // JFIF header. - header.SOI = tjei_be_word( 0xffd8 ); // Sequential DCT - header.APP0 = tjei_be_word( 0xffe0 ); - uint16_t jfif_len = sizeof( TJEJPEGHeader ) - 4 /*SOI & APP0 markers*/; - header.jfif_len = tjei_be_word( jfif_len ); - memcpy(header.jfif_id, (void*)tjeik_jfif_id, 5); - header.version = tjei_be_word( 0x0102 ); - header.units = 0x01; // Dots-per-inch - header.x_density = tjei_be_word( 0x0060 ); // 96 DPI - header.y_density = tjei_be_word( 0x0060 ); // 96 DPI - header.x_thumb = 0; - header.y_thumb = 0; - write( state, &header, sizeof( TJEJPEGHeader ), 1 ); - } - { // Write comment - TJEJPEGComment com; - uint16_t com_len = 2 + sizeof( tjeik_com_str ) - 1; - // Comment - com.com = tjei_be_word( 0xfffe ); - com.com_len = tjei_be_word( com_len ); - memcpy( com.com_str, (void*)tjeik_com_str, sizeof( tjeik_com_str )-1 ); - write( state, &com, sizeof( TJEJPEGComment ), 1 ); - } - // Write quantization tables. - write_DQT( state, state->qt_luma, 0x00 ); - write_DQT( state, state->qt_chroma, 0x01 ); - - { // Write the frame marker. - TJEFrameHeader header; - header.SOF = tjei_be_word( 0xffc0 ); - header.len = tjei_be_word( 8 + 3 * 3 ); - header.precision = 8; - assert( width <= 0xffff ); - assert( height <= 0xffff ); - header.width = tjei_be_word( (uint16_t)width ); - header.height = tjei_be_word( (uint16_t)height ); - header.numComponents = 3; - uint8_t tables[3] = { - 0, // Luma component gets luma table (see write_DQT call above.) - 1, // Chroma component gets chroma table - 1, // Chroma component gets chroma table - }; - for (int i = 0; i < 3; ++i) { - TJEComponentSpec spec; - spec.component_id = (uint8_t)( i + 1 ); // No particular reason. Just 1, 2, 3. - spec.sampling_factors = (uint8_t)0x11; - spec.qt = tables[i]; - header.component_spec[i] = spec; - } - // Write to file. - write( state, &header, sizeof(TJEFrameHeader), 1 ); - } - write_DHT( state, state->ht_bits[TJEI_LUMA_DC], state->ht_vals[TJEI_LUMA_DC], TJEI_DC, 0 ); - write_DHT( state, state->ht_bits[TJEI_LUMA_AC], state->ht_vals[TJEI_LUMA_AC], TJEI_AC, 0 ); - write_DHT( state, state->ht_bits[TJEI_CHROMA_DC], state->ht_vals[TJEI_CHROMA_DC], TJEI_DC, 1 ); - write_DHT( state, state->ht_bits[TJEI_CHROMA_AC], state->ht_vals[TJEI_CHROMA_AC], TJEI_AC, 1 ); - // Write start of scan - { - TJEScanHeader header; - header.SOS = tjei_be_word( 0xffda ); - header.len = tjei_be_word((uint16_t)( 6 + ( sizeof( TJEFrameComponentSpec ) * 3 ) ) ); - header.numComponents = 3; - uint8_t tables[3] = { - 0x00, - 0x11, - 0x11, - }; - for ( int i = 0; i < 3; ++i ) { - TJEFrameComponentSpec cs; - // Must be equal to component_id from frame header above. - cs.component_id = (uint8_t)( i + 1 ); - cs.dc_ac = (uint8_t)tables[i]; - header.component_spec[i] = cs; - } - header.first = 0; - header.last = 63; - header.ah_al = 0; - write( state, &header, sizeof( TJEScanHeader ), 1 ); - } - // Write compressed data. - float du_y[64]; - float du_b[64]; - float du_r[64]; - // Set diff to 0. - int pred_y = 0; - int pred_b = 0; - int pred_r = 0; - // Bit stack - uint32_t bitbuffer = 0; - uint32_t location = 0; - for ( int y = 0; y < height; y += 8 ) { - for ( int x = 0; x < width; x += 8 ) { - fp(y, 8, src_data, device); - // Block loop: ==== - for ( int off_y = 0; off_y < 8; ++off_y ) { - for ( int off_x = 0; off_x < 8; ++off_x ) { - int block_index = ( off_y * 8 + off_x ); - int src_index = ( ( off_y * width ) + ( x + off_x ) ) * srcNumComponents; - int col = x + off_x; - int row = y + off_y; - if( row >= height ) { - src_index -= ( width * ( row - height + 1 ) ) * srcNumComponents; - } - if( col >= width ) { - src_index -= ( col - width + 1 ) * srcNumComponents; - } - assert( src_index < width * height * srcNumComponents ); - uint8_t r = src_data[src_index + 0]; - uint8_t g = src_data[src_index + 1]; - uint8_t b = src_data[src_index + 2]; - float luma = 0.299f * r + 0.587f * g + 0.114f * b - 128; - float cb = -0.1687f * r - 0.3313f * g + 0.5f * b; - float cr = 0.5f * r - 0.4187f * g - 0.0813f * b; - du_y[block_index] = luma; - du_b[block_index] = cb; - du_r[block_index] = cr; - } - } - - encodeAndWriteMCU( state, du_y, -#if TJE_USE_FAST_DCT - pqt.luma, -#else - state->qt_luma, -#endif - state->ehuffsize[TJEI_LUMA_DC], state->ehuffcode[TJEI_LUMA_DC], - state->ehuffsize[TJEI_LUMA_AC], state->ehuffcode[TJEI_LUMA_AC], - &pred_y, &bitbuffer, &location ); - encodeAndWriteMCU( state, du_b, -#if TJE_USE_FAST_DCT - pqt.chroma, -#else - state->qt_chroma, -#endif - state->ehuffsize[TJEI_CHROMA_DC], state->ehuffcode[TJEI_CHROMA_DC], - state->ehuffsize[TJEI_CHROMA_AC], state->ehuffcode[TJEI_CHROMA_AC], - &pred_b, &bitbuffer, &location ); - encodeAndWriteMCU( state, du_r, -#if TJE_USE_FAST_DCT - pqt.chroma, -#else - state->qt_chroma, -#endif - state->ehuffsize[TJEI_CHROMA_DC], state->ehuffcode[TJEI_CHROMA_DC], - state->ehuffsize[TJEI_CHROMA_AC], state->ehuffcode[TJEI_CHROMA_AC], - &pred_r, &bitbuffer, &location ); - } - } - - // Finish the image. - { // Flush - if ( location > 0 && location < 8 ) { - writeBits( state, &bitbuffer, &location, (uint16_t)( 8 - location ), 0 ); - } - } - uint16_t EOI = tjei_be_word( 0xffd9 ); - write( state, &EOI, sizeof(uint16_t), 1 ); - if ( state->outputBufferCount ) { - state->writeContext.func( state->writeContext.jpegFile, state->outputBuffer, (int)state->outputBufferCount ); - state->outputBufferCount = 0; - } - return 1; -} - - -static void writeCallback( fs::File jpegFile, void* data, int size ) { - jpegFile.write( (uint8_t*)data, size ); -} - - -// - encodeToFile_at_quality - -// -// Usage: -// Takes bitmap data and writes a JPEG-encoded image to disk. -// -// PARAMETERS -// dest_path: filename to which we will write. e.g. "out.jpg" -// quality: 3: Highest. Compression varies wildly (between 1/3 and 1/20). -// 2: Very good quality. About 1/2 the size of 3. -// 1: Noticeable. About 1/6 the size of 3, or 1/3 the size of 2. -// width, height: image size in pixels -// numComponents: 3 is RGB. 4 is RGBA. Those are the only supported values -// src_data: pointer to the pixel data. -// -// RETURN: -// 0 on error. 1 on success. -int JPEG_Encoder::encodeToFileAtQuality( const char* dest_path, const int quality, const int width, const int height, const int numComponents, unsigned char* src_data, jpeg_encoder_callback_t fp, void* device ) { - jpegFile = _fileSystem->open( dest_path, FILE_WRITE ); - if ( !jpegFile ) { - tje_log( "Could not open file for writing." ); - return 0; - } - /*int result = */encodeWithFunc( writeCallback, jpegFile, quality, width, height, numComponents, src_data, fp, device ); - jpegFile.close(); - return _fileSystem->exists( dest_path ); -} - -#pragma GCC diagnostic pop diff --git a/src/utility/ScreenShotService/JPG/TinyJPEGEncoder.hpp b/src/utility/ScreenShotService/JPG/TinyJPEGEncoder.hpp deleted file mode 100644 index 1693015..0000000 --- a/src/utility/ScreenShotService/JPG/TinyJPEGEncoder.hpp +++ /dev/null @@ -1,224 +0,0 @@ -/* - * - * TinyJPEGEncoder - * - * Copyright 2019 tobozo http://github.com/tobozo - * - * Permission is hereby granted, free of charge, to any person - * obtaining a copy of this software and associated documentation - * files ("TinyBMPEncoder"), to deal in the Software without - * restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following - * conditions: - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - * OTHER DEALINGS IN THE SOFTWARE. - * - * This is a readable and simple single-header JPEG encoder. - * - * Features - * - Implements Baseline DCT JPEG compression. - * - No dynamic allocations. - * - Writes to SD/SD_MMC/SPIFFS - * - * ==== Thanks ==== - * - * AssociationSirius (Bug reports) - * Bernard van Gastel (Thread-safe defaults, BSD compilation) - * Sergio Gonzalez (initial C/std library https://github.com/serge-rgb/TinyJPEG) - * - * ==== License ==== - * - * This software is in the public domain. Where that dedication is not - * recognized, you are granted a perpetual, irrevocable license to copy and - * modify this file as you see fit. - * - * === CHANGES by tobozo Nov. 2019=== - * - * - uses psram if available - * - uses fs::FS - * - rewrotecode to in a C++ style - * - */ - - -#pragma once -#define __TINY_JPEG_ENCODER__ - -#include "../ScreenShot.hpp" - -// C std lib -#include -#include -#include // floorf, ceilf -#include // FILE, puts -#include // memcpy - -// Only use zero for debugging and/or inspection. -#define TJE_USE_FAST_DCT 1 - -#define TJEI_BUFFER_SIZE 512 -#ifndef NDEBUG - #define tje_log(msg) log_e(msg) -#else // NDEBUG - #define tje_log(msg) -#endif // NDEBUG - -// ============================================================ -// The following structs exist only for code clarity, debugability, and -// readability. They are used when writing to disk, but it is useful to have -// 1-packed-structs to document how the format works, and to inspect memory -// while developing. -// ============================================================ - -static const uint8_t tjeik_jfif_id[] = "JFIF"; -static const uint8_t tjeik_com_str[] = "Created by Tiny JPEG Encoder (https://github.com/serge-rgb/TinyJPEG)"; - -typedef void writeFunc( fs::File jpegFile, void* data, int size ); - -typedef struct { - fs::File jpegFile; - writeFunc* func; -} TJEWriteContext; - -typedef struct { - // Huffman data. - uint8_t ehuffsize[4][257]; - uint16_t ehuffcode[4][256]; - uint8_t const * ht_bits[4]; - uint8_t const * ht_vals[4]; - // Cuantization tables. - uint8_t qt_luma[64]; - uint8_t qt_chroma[64]; - // fwrite by default. User-defined when using encodeWithFunc. - TJEWriteContext writeContext; - // Buffered output. Big performance win when using the usual stdlib implementations. - size_t outputBufferCount; - uint8_t outputBuffer[TJEI_BUFFER_SIZE]; -} TJEState; - -// TODO: Get rid of packed structs! -#pragma pack(push) -#pragma pack(1) -typedef struct { - uint16_t SOI; - // JFIF header. - uint16_t APP0; - uint16_t jfif_len; - uint8_t jfif_id[5]; - uint16_t version; - uint8_t units; - uint16_t x_density; - uint16_t y_density; - uint8_t x_thumb; - uint8_t y_thumb; -} TJEJPEGHeader; - -typedef struct { - uint16_t com; - uint16_t com_len; - char com_str[sizeof(tjeik_com_str) - 1]; -} TJEJPEGComment; - -// Helper struct for TJEFrameHeader (below). -typedef struct { - uint8_t component_id; - uint8_t sampling_factors; // most significant 4 bits: horizontal. 4 LSB: vertical (A.1.1) - uint8_t qt; // Quantization table selector. -} TJEComponentSpec; - -typedef struct { - uint16_t SOF; - uint16_t len; // 8 + 3 * frame.numComponents - uint8_t precision; // Sample precision (bits per sample). - uint16_t height; - uint16_t width; - uint8_t numComponents; // For this implementation, will be equal to 3. - TJEComponentSpec component_spec[3]; -} TJEFrameHeader; - -typedef struct { - uint8_t component_id; // Just as with TJEComponentSpec - uint8_t dc_ac; // (dc|ac) -} TJEFrameComponentSpec; - -typedef struct { - uint16_t SOS; - uint16_t len; - uint8_t numComponents; // 3. - TJEFrameComponentSpec component_spec[3]; - uint8_t first; // 0 - uint8_t last; // 63 - uint8_t ah_al; // o -} TJEScanHeader; - -enum { - TJEI_LUMA_DC, - TJEI_LUMA_AC, - TJEI_CHROMA_DC, - TJEI_CHROMA_AC, -}; - -#if TJE_USE_FAST_DCT -struct TJEProcessedQT { - float chroma[64]; - float luma[64]; -}; -#endif - -typedef enum { - TJEI_DC = 0, - TJEI_AC = 1 -} TJEHuffmanTableClass; - - -#pragma pack(pop) - -typedef void (*jpeg_encoder_callback_t)(uint32_t y, uint32_t h, unsigned char* rgb_buffer, void* device); - -class JPEG_Encoder { - public: - - JPEG_Encoder( fs::FS * fileSystem ) : _fileSystem( fileSystem ) { } - - fs::File jpegFile; - fs::FS * _fileSystem; - - uint8_t** huffsize = NULL; - uint16_t** huffcode = NULL; - - void begin( bool ifPsram = true ); - int encodeToFile( const char* dest_path, const int width, const int height, const int numComponents, unsigned char* src_data, jpeg_encoder_callback_t fp, void* device ); - int encodeWithFunc( writeFunc* func, fs::File jpegFile, const int quality, const int width, const int height, const int numComponents, unsigned char* src_data, jpeg_encoder_callback_t fp, void* device ); - - private: - - int encodeMain( TJEState* state, unsigned char* src_data, jpeg_encoder_callback_t fp, void* device, const int width, const int height, const int srcNumComponents ); - int encodeToFileAtQuality( const char* dest_path, const int quality, const int width, const int height, const int numComponents, unsigned char* src_data, jpeg_encoder_callback_t fp, void* device ); - void encodeAndWriteMCU( TJEState*, float*, float*, uint8_t*, uint16_t*, uint8_t*, uint16_t*, int*, uint32_t*, uint32_t* ); - void write( TJEState* state, const void* data, size_t num_bytes, size_t num_elements ); - void write_DQT( TJEState* state, const uint8_t* matrix, uint8_t id ); - void write_DHT( TJEState* state, uint8_t const * matrix_len, uint8_t const * matrix_val, TJEHuffmanTableClass ht_class, uint8_t id ); - void writeBits( TJEState* state, uint32_t* bitbuffer, uint32_t* location, uint16_t num_bits, uint16_t bits ); - - uint8_t* huffGetCodeLengths( uint8_t huffsize[/*256*/], uint8_t const * bits ); - uint16_t* huffGetCodes( uint16_t codes[], uint8_t* huffsize, int64_t count ); - void huffGetExtended( uint8_t* out_ehuffsize, uint16_t* out_ehuffcode, uint8_t const * huffval, uint8_t* huffsize, uint16_t* huffcode, int64_t count ); - void huffExpand( TJEState* state ); - void calcVarLengthAsInt( int value, uint16_t out[2] ); - void fast_fdct( float * data ); - - -}; // end class - diff --git a/src/utility/ScreenShotService/QOI/QOIEncoder.cpp b/src/utility/ScreenShotService/QOI/QOIEncoder.cpp index 82ff762..55eabd7 100644 --- a/src/utility/ScreenShotService/QOI/QOIEncoder.cpp +++ b/src/utility/ScreenShotService/QOI/QOIEncoder.cpp @@ -37,22 +37,23 @@ #endif -struct quoienc_t -{ - LGFX* display; - int x, y, w, h; -}; - +Stream* QOI_Encoder::stream = nullptr; -static Stream* _qoi_dest = nullptr; +uint8_t* QOI_Encoder::get_row_sprite( uint8_t *lineBuffer, int flip, int w, int h, int y, void *qoienc ) +{ + assert( qoienc ); + auto qoi_info = (qoienc_t*)qoienc; + ((LGFX_Sprite*)(qoi_info->device))->readRectRGB( qoi_info->x, qoi_info->y + y, w, h, lineBuffer ); + return lineBuffer; +} -uint8_t* QOI_Encoder::get_row( uint8_t *lineBuffer, int flip, int w, int h, int y, void *qoienc ) +uint8_t* QOI_Encoder::get_row_tft( uint8_t *lineBuffer, int flip, int w, int h, int y, void *qoienc ) { assert( qoienc ); - auto qoi_info = (quoienc_t*)qoienc; - qoi_info->display->readRectRGB( qoi_info->x, qoi_info->y + y, w, h, lineBuffer ); + auto qoi_info = (qoienc_t*)qoienc; + ((LGFX*)(qoi_info->device))->readRectRGB( qoi_info->x, qoi_info->y + y, w, h, lineBuffer ); return lineBuffer; } @@ -60,51 +61,65 @@ uint8_t* QOI_Encoder::get_row( uint8_t *lineBuffer, int flip, int w, int h, int int QOI_Encoder::write_bytes(uint8_t* buf, size_t buf_len) { - assert( _qoi_dest ); - return _qoi_dest->write( buf, buf_len ); + assert( QOI_Encoder::stream ); + return QOI_Encoder::stream->write( buf, buf_len ); } +size_t QOI_Encoder::encode( Stream* stream, qoienc_t *qoi_info ) +{ + assert(stream); + assert(qoi_info); + QOI_Encoder::stream = stream; + RGBColor lineBuffer[qoi_info->w+1]; + return lgfx_qoi_encoder_write_cb( &lineBuffer, 4096, qoi_info->w, qoi_info->h, 3, 0, is_sprite?get_row_sprite:get_row_tft, write_bytes, qoi_info ); +} +size_t QOI_Encoder::encodeToStream( Stream* stream, const int w, const int h ) +{ + return encodeToStream( stream, 0, 0, w, h ); +} -void QOI_Encoder::init() +size_t QOI_Encoder::encodeToStream( Stream* stream, const int x, const int y, const int w, const int h ) { - // if( !psramInit() ) { - // log_n("[INFO] No PSRAM found, PNG Encoding won't work"); - // } + assert( stream ); + qoienc_t qoi_info; + qoi_info.device = _src; + qoi_info.x = x; + qoi_info.y = y; + qoi_info.w = w; + qoi_info.h = h; + return encode( stream, &qoi_info ); } -bool QOI_Encoder::encodeToFile( const char* filename, const int imageW, const int imageH ) +size_t QOI_Encoder::encodeToFile( const char* filename, const int w, const int h ) { - return encodeToFile( filename, 0, 0, imageW, imageH ); + return encodeToFile( filename, 0, 0, w, h ); } -bool QOI_Encoder::encodeToFile( const char* filename, const int imageX, const int imageY, const int imageW, const int imageH ) +size_t QOI_Encoder::encodeToFile( const char* filename, const int x, const int y, const int w, const int h ) { - quoienc_t qoi_info; - qoi_info.display = _tft; - qoi_info.x = imageX; - qoi_info.y = imageY; - qoi_info.w = imageW; - qoi_info.h = imageH; - + assert( filename ); + assert( _fileSystem ); + qoienc_t qoi_info; + qoi_info.device = _src; + qoi_info.x = x; + qoi_info.y = y; + qoi_info.w = w; + qoi_info.h = h; fs::File destFile = _fileSystem->open( filename, "w"); if( !destFile ) { log_e( "Can't write capture file %s, make sure the path exists!", filename ); - return false; + return 0; } - log_e( "Opened capture file %s for writing...", filename ); - _qoi_dest = (Stream*)&destFile; - RGBColor lineBuffer[imageW+1]; - //uint8_t* lineBuffer = (uint8_t*)calloc(4096, sizeof(uint8_t)); - size_t written_bytes = lgfx_qoi_encoder_write_cb( &lineBuffer, 4096, imageW, imageH, 3, 0, get_row, write_bytes, &qoi_info ); - //free( lineBuffer ); + log_v( "Opened capture file %s for writing...", filename ); + size_t written_bytes = encode( &destFile, &qoi_info ); destFile.close(); - return written_bytes > 0; + return written_bytes; } diff --git a/src/utility/ScreenShotService/QOI/QOIEncoder.hpp b/src/utility/ScreenShotService/QOI/QOIEncoder.hpp index 124f90e..82847c4 100644 --- a/src/utility/ScreenShotService/QOI/QOIEncoder.hpp +++ b/src/utility/ScreenShotService/QOI/QOIEncoder.hpp @@ -31,20 +31,36 @@ #include "../ScreenShot.hpp" +struct qoienc_t +{ + void* device; // display or sprite + int32_t x, y; + uint32_t w, h; +}; + class QOI_Encoder { public: - QOI_Encoder( LGFX *tft, fs::FS *fileSystem ) : _tft(tft), _fileSystem(fileSystem) { }; - void init(); - bool encodeToFile( const char* filename, const int imageW, const int imageH ); - bool encodeToFile( const char* filename, const int imageX, const int imageY, const int imageW, const int imageH ); - static uint8_t* get_row( uint8_t *lineBuffer, int flip, int w, int h, int y, void *qoienc ); + + QOI_Encoder( void* src, bool is_sprite ) : _src(src), is_sprite(is_sprite) { }; + QOI_Encoder( LGFX *tft, fs::FS *fileSystem ) : _src(tft), _fileSystem(fileSystem) { }; + QOI_Encoder( LGFX_Sprite *sprite, fs::FS *fileSystem ) : _src(sprite), _fileSystem(fileSystem) { is_sprite = true; }; + void setFileSystem( fs::FS* fs ) { _fileSystem=fs; } + size_t encode( Stream* stream, qoienc_t *qoi_info ); + size_t encodeToStream( Stream* stream, const int imageW, const int imageH ); + size_t encodeToStream( Stream* stream, const int imageX, const int imageY, const int imageW, const int imageH ); + size_t encodeToFile( const char* filename, const int imageW, const int imageH ); + size_t encodeToFile( const char* filename, const int imageX, const int imageY, const int imageW, const int imageH ); + static uint8_t* get_row_tft( uint8_t *lineBuffer, int flip, int w, int h, int y, void *qoienc ); + static uint8_t* get_row_sprite( uint8_t *lineBuffer, int flip, int w, int h, int y, void *qoienc ); static int write_bytes(uint8_t* buf, size_t buf_len); + static Stream* stream; private: - LGFX *_tft; + void *_src; // display or sprite + bool is_sprite = false; fs::FS * _fileSystem; }; diff --git a/src/utility/ScreenShotService/ScreenShot.cpp b/src/utility/ScreenShotService/ScreenShot.cpp index 41f1cbb..68f3897 100644 --- a/src/utility/ScreenShotService/ScreenShot.cpp +++ b/src/utility/ScreenShotService/ScreenShot.cpp @@ -29,16 +29,11 @@ #include "ScreenShot.hpp" -/* -ScreenShotService::ScreenShotService( M5Display *tft, fs::FS &fileSystem ) -{ - // wut ? -} -*/ + ScreenShotService::~ScreenShotService() { if( _begun ) { - free( rgbBuffer ); + if( screenShotBuffer) free( screenShotBuffer ); _begun = false; } } @@ -46,8 +41,7 @@ ScreenShotService::~ScreenShotService() void ScreenShotService::init() { if( _inited ) return; - // default capture mode is full screen - setWindow(0, 0, _tft->width(), _tft->height() ); + setWindow(0, 0, _src->width(), _src->height() ); // default capture mode is full screen _inited = true; } @@ -55,22 +49,12 @@ void ScreenShotService::init() void ScreenShotService::setWindow( uint32_t x, uint32_t y, uint32_t w, uint32_t h ) { - if( x >= _tft->width()-1 ) { - x = 0; - } - if( y >= _tft->height()-1 ) { - y = 0; - } - if( x+w > _tft->width() ) { - w = _tft->width()-x; - } - if( y+h > _tft->height() ) { - h = _tft->height()-y; - } - _x = x; - _y = y; - _w = w; - _h = h; + if( x >= _src->width()-1 ) x = 0; + if( y >= _src->height()-1 ) y = 0; + if( x+w > _src->width() ) w = _src->width()-x; + if( y+h > _src->height() ) h = _src->height()-y; + _x = x; _y = y; + _w = w; _h = h; } @@ -82,7 +66,7 @@ bool ScreenShotService::begin( bool ifPsram ) } if( _begun ) return true; if( !displayCanReadPixels() ) { - log_n( "readPixel() test failed, screenshots are disabled" ); + log_i( "readPixel() test failed, screenshots are disabled" ); return false; } _psram = ifPsram; @@ -93,19 +77,19 @@ bool ScreenShotService::begin( bool ifPsram ) bool ScreenShotService::displayCanReadPixels() { - uint16_t value_initial = _tft->readPixel( 30,30 ); + uint16_t value_initial = _src->readPixel( 30,30 ); uint8_t r = 64, g = 255, b = 128; // scan color - uint16_t value_in = _tft->color565(r, g, b); + uint16_t value_in = _src->color565(r, g, b); uint16_t value_out; __attribute__((unused)) byte testnum = 0; log_d( "Testing display readpixel" ); // log_w( "Testing display#%04x", TFT_DRIVER ); - _tft->drawPixel( 30, 30, value_in ); // <----- Test color - value_out = _tft->readPixel( 30,30 ); + _src->drawPixel( 30, 30, value_in ); // <----- Test color + value_out = _src->readPixel( 30,30 ); log_d( "test#%d: readPixel(as rgb565), expected:0x%04x, got: 0x%04x", testnum++, value_in, value_out ); if( value_in == value_out ) { readPixelSuccess = true; - _tft->drawPixel( 30, 30, value_initial ); + _src->drawPixel( 30, 30, value_initial ); return true; } return false; @@ -115,90 +99,271 @@ bool ScreenShotService::displayCanReadPixels() void ScreenShotService::snap( const char* name, bool displayAfter ) { if( readPixelSuccess == false ) { - log_n( "[ERROR] This TFT is unsupported, or it hasn't been tested yet" ); + log_i( "[ERROR] This TFT is unsupported, or it hasn't been tested yet" ); return; } if( jpegCapture ) { // enough ram allocated? snapJPG( name, displayAfter ); - } else { // BMP capture - snapBMP( name, displayAfter ); + } else { // PNG capture + snapPNG( name, displayAfter ); } return; } -// this is lame -static uint32_t jpeg_encoder_xoffset = 0; -static uint32_t jpeg_encoder_yoffset = 0; -static uint32_t jpeg_encoder_w = 0; -static void jpeg_encoder_callback(uint32_t y, uint32_t h, unsigned char* rgbBuffer, void* device) -{ - auto tft = (LGFX*)device; - tft->readRectRGB( jpeg_encoder_xoffset, jpeg_encoder_yoffset+y, jpeg_encoder_w, h, rgbBuffer ); -} +#if defined HAS_JPEGENC + + // for direct to disk (no jpeg buffer) image encoding + struct JPGENC_IMG_Proxy_t + { + static fs::FS* fs; + static void* open(const char *filename) { static auto f = fs->open(filename, FILE_WRITE); return (void *)&f; } + static void close(JPEGFILE *p) { File *f = (File *)p->fHandle; if (f) f->close(); } + static int32_t read(JPEGFILE *p, uint8_t *buffer, int32_t length) { File *f = (File *)p->fHandle; return f->read(buffer, length); } + static int32_t write(JPEGFILE *p, uint8_t *buffer, int32_t length) { File *f = (File *)p->fHandle; return f->write(buffer, length); } + static int32_t seek(JPEGFILE *p, int32_t position) { File *f = (File *)p->fHandle; return f->seek(position); } + }; + + fs::FS* JPGENC_IMG_Proxy_t::fs = nullptr; + + + void ScreenShotService::snapJPG( const char* name, bool displayAfter ) + { + if( !_begun || !_inited ) return; + assert( name ); + assert( _fileSystem ); + + JPEG jpg; + JPEGENCODE jpe; + JPGENC_IMG_Proxy_t::fs = _fileSystem; + JPGENC_IMG_Proxy_t IMG_Proxy = JPGENC_IMG_Proxy_t(); + bool use_buffer = true; + auto color_depth = _src->getColorDepth(); + const int bytes_per_pixel = color_depth/8; + uint8_t jpeg_pixel_format = color_depth >= 24 ? JPEG_PIXEL_RGB888 : JPEG_PIXEL_RGB565; + uint8_t ucMCU[64*bytes_per_pixel]; + int iMCUCount, rc, i; + size_t iDataSize; + [[maybe_unused]] uint32_t time_start = millis(); + + genFileName( name, "jpg" ); // store computed file name in 'fileName' (char[255]) + screenShotBufSize = _w*_h*.5; // will use buffered writes (faster), estimated max jpeg bytes = (w*h)/2, i.e. max 4bits per pixel + + if( _psram && psramInit() ) { + screenShotBuffer = (uint8_t*)ps_calloc( screenShotBufSize, sizeof( uint8_t ) ); + } else { + screenShotBuffer = (uint8_t*)calloc( screenShotBufSize, sizeof( uint8_t ) ); + _psram = false; + } + if( screenShotBuffer ) { + log_v( "ScreenShot Service can use JPG capture" ); + } else { + log_i( "Not enough ram to use jpeg screenshot" ); + use_buffer = false; + } -void ScreenShotService::snapJPG( const char* name, bool displayAfter ) -{ - if( !jpegCapture ) return; - jpeg_encoder_xoffset = _x; - jpeg_encoder_yoffset = _y; - jpeg_encoder_w = _w; - genFileName( name, "jpg" ); - uint32_t time_start = millis(); - - if( _psram && psramInit() ) { - log_v("Will attempt to allocate psram for screenshots"); - rgbBuffer = (uint8_t*)ps_calloc( (_w*8*3)+1, sizeof( uint8_t ) ); - } else { - log_v("Will attempt to allocate ram for screenshots"); - rgbBuffer = (uint8_t*)calloc( (_w*8*3)+1, sizeof( uint8_t ) ); - _psram = false; - } - if( rgbBuffer != NULL ) { - log_v( "ScreenShot Service can use JPG capture" ); - } else { - log_n( "Not enough ram to use jpeg screenshot" ); - jpegCapture = false; - return; - } + if( use_buffer && screenShotBufSize > 0 ) { + rc = jpg.open(screenShotBuffer, screenShotBufSize); + } else { + rc = jpg.open(fileName, IMG_Proxy.open, IMG_Proxy.close, IMG_Proxy.read, IMG_Proxy.write, IMG_Proxy.seek); + } - JPEGEncoder = new JPEG_Encoder( _fileSystem ); - JPEGEncoder->begin( _psram ); + if (rc != JPEG_SUCCESS) { + log_e("Failed to create jpeg object"); + goto _end; + } + + rc = jpg.encodeBegin(&jpe, _w, _h, jpeg_pixel_format, JPEG_SUBSAMPLE_444, JPEG_Q_HIGH); + + if (rc != JPEG_SUCCESS) { + log_e("Failed to initiate jpeg encoding"); + goto _end; + } + + memset(ucMCU, 0, sizeof(ucMCU)); // zerofill MCU, not really required as we'll fill it soon with readRect() + iMCUCount = ((_w + jpe.cx-1)/ jpe.cx) * ((_h + jpe.cy-1) / jpe.cy); + + for (i=0; i= 24 ) _src->readRectRGB( _x+jpe.x, _y+jpe.y, jpe.cx, jpe.cy, ucMCU ); + else _src->readRect( _x+jpe.x, _y+jpe.y, jpe.cx, jpe.cy, (lgfx::rgb565_t*)ucMCU ); + rc = jpg.addMCU(&jpe, ucMCU, 8*bytes_per_pixel); + } + + iDataSize = jpg.close(); + + if( use_buffer ) { + auto file = _fileSystem->open(fileName, "w" ); + if( ! file ) { + log_e("Can't open %d for writing", fileName ); + goto _end; + } + file.write( screenShotBuffer, iDataSize ); + file.close(); + } + + log_i( "[SUCCESS] Screenshot saved as %s (%d bytes) in %u ms", fileName, iDataSize, millis()-time_start); - if ( !JPEGEncoder->encodeToFile( fileName, _w, _h, 3 /*3=RGB,4=RGBA*/, rgbBuffer, &jpeg_encoder_callback, _tft ) ) { - log_n( "[ERROR] Could not write JPG file to: %s", fileName ); - } else { - fs::File outFile = _fileSystem->open( fileName ); - size_t fileSize = outFile.size(); - outFile.close(); - log_n( "[SUCCESS] Screenshot saved as %s (%d bytes). Total time %u ms", fileName, fileSize, millis()-time_start); if( displayAfter ) { snapAnimation(); - _tft->drawJpgFile( *_fileSystem, fileName, _x, _y, _w, _h, 0, 0 ); + _src->drawJpgFile( *_fileSystem, fileName, _x, _y ); delay(5000); } + + _end: + + if( screenShotBuffer ) free( screenShotBuffer ); } - delete JPEGEncoder; - free( rgbBuffer ); -} + + + + void ScreenShotService::snapAVI( AVI_Params_t *params, bool finalize ) + { + assert( params ); + assert( _fileSystem || params->fs ); + assert( _src ); // source display/sprite + + if( params->fs ) _fileSystem = params->fs; + else params->fs = _fileSystem; + + if( !_begun || !_inited ) return; + _psram = _psram && psramInit(); + + auto color_depth = _is_sprite ? ((LGFX_Sprite*)_src)->getColorDepth() : ((LGFX*)_src)->getColorDepth(); + auto readRect = _is_sprite ? defaultReadRect : defaultReadRect; + + if( params->size.w>0 && params->size.h > 0 ) { + _w = params->size.w; + _h = params->size.h; + } else { + _w = _is_sprite ? ((LGFX_Sprite*)_src)->width() : ((LGFX*)_src)->width(); + _h = _is_sprite ? ((LGFX_Sprite*)_src)->height() : ((LGFX*)_src)->height(); + params->size.w = _w; + params->size.h = _h; + } + + assert( _w > 0 ); + assert( _h > 0 ); + + //static fs::File aviFile; + static AviMjpegEncoder* AVIEncoder = nullptr; + static JPGENC_AVI_Proxy_t AVI_Proxy; // callbacks for JPEGENC + + const int bytes_per_pixel = color_depth/8; + uint8_t jpeg_pixel_format = color_depth >= 24 ? JPEG_PIXEL_RGB888 : JPEG_PIXEL_RGB565; + + JPEG jpg; + JPEGENCODE jpe; + uint16_t MCUSize = 64*bytes_per_pixel; + static uint8_t *ucMCU = nullptr;// (uint8_t*)calloc(MCUSize, sizeof(uint8_t)); + int iMCUCount, rc, i; + char framename[32]; + + if( AVIEncoder == nullptr ) { + if( !_fileSystem ) { + log_e("No file/stream provided"); + goto _end; + } + + if( params->path == "" ) { + genFileName( "out", "avi" ); + params->path = String( fileName ); + } + + AVIEncoder = new AviMjpegEncoder( params ); + + if( !params->ready ) { + log_e("Unable to create AVIEncoder instance"); + goto _end; + } + + JPGENC_AVI_Proxy_t::aviEncoder = AVIEncoder; + screenShotBufSize = params->use_buffer ? _w*_h*.5 : 0; // estimated max jpeg bytes = (w*h)/2, i.e. max 4bits per pixel + if( params->use_buffer && screenShotBufSize > 0 ) { // allocate some memory for the buffer + screenShotBuffer = _psram ? (uint8_t*)ps_calloc( screenShotBufSize, sizeof(uint8_t) ) : (uint8_t*)calloc( screenShotBufSize, sizeof(uint8_t) ); + if( ! screenShotBuffer ) { + log_e("Failed to allocate jpeg buffer (%d bytes), JPEGENC will use unbuffered (slower) writes instead", screenShotBufSize); + screenShotBufSize = 0; + params->use_buffer = false; + _psram = false; + } + } + + log_d("Capture params: %s[%dx%d]@%dbpp (jpg_bufsize=%d, %s)", _is_sprite?"sprite":"display", _w, _h, color_depth, screenShotBufSize, _psram?"PSRAM":"Heap" ); + + ucMCU = (uint8_t*)calloc(MCUSize, sizeof(uint8_t)); + if( !ucMCU ) { + log_e("Can't allocate %d byte for JPEG MCUs", MCUSize ); + goto _end; + } + } // end if( AVIEncoder == nullptr ) + + if( finalize ) { + if( AVIEncoder ) AVIEncoder->finalize(); + _end: + if( ucMCU ) free( ucMCU ); + if( AVIEncoder ) { + delete AVIEncoder; + AVIEncoder = nullptr; + } + if( screenShotBuffer ) free( screenShotBuffer ); + params->ready = false; + return; + } + + if( params->use_buffer && screenShotBufSize > 0 ) { + rc = jpg.open(screenShotBuffer, screenShotBufSize); + } else { + sprintf( framename, "frame-%d", AVIEncoder->framesCount()+1 ); + rc = jpg.open(framename, AVI_Proxy.open, AVI_Proxy.close, AVI_Proxy.read, AVI_Proxy.write, AVI_Proxy.seek); + } + + if (rc != JPEG_SUCCESS) { + log_e("Failed to create jpeg object"); + goto _end; + } + + rc = jpg.encodeBegin(&jpe, _w, _h, jpeg_pixel_format, JPEG_SUBSAMPLE_444, JPEG_Q_BEST); + + if (rc != JPEG_SUCCESS) { + log_e("Failed to initiate jpeg encoding"); + goto _end; + } + + iMCUCount = ((_w + jpe.cx-1)/ jpe.cx) * ((_h + jpe.cy-1) / jpe.cy); + + for (i=0; iuse_buffer && screenShotBufSize > 0 ) { // if using buffered writes, send the jpeg buffer + AVIEncoder->addJpegFrame( screenShotBuffer, iDataSize ); + } + } + +#endif + + + void ScreenShotService::snapBMP( const char* name, bool displayAfter ) { genFileName( name, "bmp" ); - uint32_t time_start = millis(); - BMPEncoder = new BMP_Encoder( _tft, _fileSystem ); + [[maybe_unused]] uint32_t time_start = millis(); + BMP_Encoder *BMPEncoder = new BMP_Encoder( _src, _fileSystem ); if( !BMPEncoder->encodeToFile( fileName, _x, _y, _w, _h ) ) { log_e( "[ERROR] Could not write BMP file to: %s", fileName ); } else { fs::File outFile = _fileSystem->open( fileName ); - size_t fileSize = outFile.size(); + log_i( "[SUCCESS] Screenshot saved as %s (%d bytes). Total time %u ms", fileName, outFile.size(), millis()-time_start); outFile.close(); - log_n( "[SUCCESS] Screenshot saved as %s (%d bytes). Total time %u ms", fileName, fileSize, millis()-time_start); if( displayAfter ) { snapAnimation(); - _tft->drawBmpFile( *_fileSystem, fileName, _x, _y ); + _src->drawBmpFile( *_fileSystem, fileName, _x, _y ); delay(5000); } } @@ -209,19 +374,18 @@ void ScreenShotService::snapBMP( const char* name, bool displayAfter ) void ScreenShotService::snapPNG( const char* name, bool displayAfter ) { genFileName( name, "png" ); - uint32_t time_start = millis(); - PNGEncoder = new PNG_Encoder( _tft, _fileSystem ); + [[maybe_unused]] uint32_t time_start = millis(); + PNG_Encoder* PNGEncoder = new PNG_Encoder( _src, _fileSystem ); PNGEncoder->init(); if( !PNGEncoder->encodeToFile( fileName, _x, _y, _w, _h ) ) { log_e( "[ERROR] Could not write PNG file to: %s", fileName ); } else { fs::File outFile = _fileSystem->open( fileName ); - size_t fileSize = outFile.size(); + log_i( "[SUCCESS] Screenshot saved as %s (%d bytes). Total time %u ms", fileName, outFile.size(), millis()-time_start); outFile.close(); - log_n( "[SUCCESS] Screenshot saved as %s (%d bytes). Total time %u ms", fileName, fileSize, millis()-time_start); if( displayAfter ) { snapAnimation(); - _tft->drawPngFile( *_fileSystem, fileName, _x, _y ); + _src->drawPngFile( *_fileSystem, fileName, _x, _y ); delay(5000); } } @@ -232,44 +396,56 @@ void ScreenShotService::snapPNG( const char* name, bool displayAfter ) void ScreenShotService::snapGIF( const char* name, bool displayAfter ) { genFileName( name, "gif" ); - uint32_t time_start = millis(); - GIFEncoder = new GIF_Encoder( _tft, _fileSystem ); + [[maybe_unused]] uint32_t time_start = millis(); + GIF_Encoder* GIFEncoder = new GIF_Encoder( _src, _fileSystem ); if( !GIFEncoder->encodeToFile( fileName, _x, _y, _w, _h ) ) { log_e( "[ERROR] Could not write GIF file to: %s", fileName ); } else { fs::File outFile = _fileSystem->open( fileName ); - size_t fileSize = outFile.size(); + log_i( "[SUCCESS] Screenshot saved as %s (%d bytes). Total time %u ms", fileName, outFile.size(), millis()-time_start); outFile.close(); - log_n( "[SUCCESS] Screenshot saved as %s (%d bytes). Total time %u ms", fileName, fileSize, millis()-time_start); - //if( displayAfter ) { - // snapAnimation(); - // _tft->drawGifFile( *_fileSystem, fileName, 0, 0 ); - // delay(5000); - //} } delete GIFEncoder; } -void ScreenShotService::snapQOI( const char* name, bool displayAfter ) +size_t ScreenShotService::snapQOI( Stream* stream ) +{ + assert( stream ); + [[maybe_unused]] uint32_t time_start = millis(); + QOI_Encoder* QOIEncoder = new QOI_Encoder( _src, _is_sprite ); + size_t qoi_size = QOIEncoder->encodeToStream( stream, _x, _y, _w, _h ); + if( qoi_size <= 0 ) { + log_e( "[ERROR] Could not stream QOI data ([%d:%d]@[%dx%d] is sprite: %s)", _x, _y, _w, _h, _is_sprite?"true":"false" ); + } else { + log_v( "[SUCCESS] Screenshot streamed (%d bytes). Total time %u ms", qoi_size, millis()-time_start); + } + delete QOIEncoder; + return qoi_size; +} + + +size_t ScreenShotService::snapQOI( const char* name, bool displayAfter ) { genFileName( name, "qoi" ); - uint32_t time_start = millis(); - QOIEncoder = new QOI_Encoder( _tft, _fileSystem ); - if( !QOIEncoder->encodeToFile( fileName, _x, _y, _w, _h ) ) { + [[maybe_unused]] uint32_t time_start = millis(); + QOI_Encoder* QOIEncoder = new QOI_Encoder( _src, _is_sprite ); + QOIEncoder->setFileSystem(_fileSystem); + size_t qoi_size = QOIEncoder->encodeToFile( fileName, _x, _y, _w, _h ); + if( qoi_size <= 0 ) { log_e( "[ERROR] Could not write QOI file to: %s", fileName ); } else { fs::File outFile = _fileSystem->open( fileName ); - size_t fileSize = outFile.size(); + log_i( "[SUCCESS] Screenshot saved as %s (%d bytes). Total time %u ms", fileName, outFile.size(), millis()-time_start); outFile.close(); - log_n( "[SUCCESS] Screenshot saved as %s (%d bytes). Total time %u ms", fileName, fileSize, millis()-time_start); if( displayAfter ) { snapAnimation(); - _tft->drawQoiFile( *_fileSystem, fileName, _x, _y ); + _src->drawQoiFile( *_fileSystem, fileName, _x, _y ); delay(5000); } } delete QOIEncoder; + return qoi_size; } @@ -299,22 +475,21 @@ void ScreenShotService::genFileName( const char* name, const char* extension ) struct tm now; getLocalTime( &now, 0 ); sprintf( fileName, "%s/%s-%04d-%02d-%02d_%02dh%02dm%02ds.%s", folderName, name, (now.tm_year)+1900,( now.tm_mon)+1, now.tm_mday,now.tm_hour , now.tm_min, now.tm_sec, extension ); - log_d( "has prefix: %s, has folder:%s, has extension: /%s, got fileName: %s", name, folderName, extension, fileName ); } else { - log_d( "has path: %s", name ); sprintf( fileName, "%s", name ); } + log_v( "generated path: %s", fileName ); } void ScreenShotService::snapAnimation() { for( byte i = 0; i<16; i++ ) { - _tft->drawRect(0, 0, _tft->width()-1, _tft->height()-1, TFT_WHITE); + _src->drawRect(0, 0, _src->width()-1, _src->height()-1, TFT_WHITE); delay(20); - _tft->drawRect(0, 0, _tft->width()-1, _tft->height()-1, TFT_BLACK); + _src->drawRect(0, 0, _src->width()-1, _src->height()-1, TFT_BLACK); delay(20); } - _tft->clear(); + _src->clear(); delay(150); } diff --git a/src/utility/ScreenShotService/ScreenShot.hpp b/src/utility/ScreenShotService/ScreenShot.hpp index 862651e..dfafefc 100644 --- a/src/utility/ScreenShotService/ScreenShot.hpp +++ b/src/utility/ScreenShotService/ScreenShot.hpp @@ -36,70 +36,105 @@ #define LGFX_USE_V1 #include -class JPEG_Encoder; -class BMP_Encoder; -class PNG_Encoder; -class GIF_Encoder; -class QOI_Encoder; +#include "./AVI/AviMjpegEncoder.hpp" + + +template // GFX can be either LGFX_Sprite or LGFX_Device +static void defaultReadRect( void* src, int32_t x, int32_t y, uint32_t w, uint32_t h, uint8_t* buf ) +{ + assert(src); + GFX* gfx = (GFX*)src; + if( gfx->getColorDepth() >= 24 ) gfx->readRectRGB( x, y, w, h, buf ); + else gfx->readRect( x, y, w, h, (lgfx::rgb565_t*)buf ); +}; + + + class ScreenShotService { public: - ScreenShotService( LGFX *tft, fs::FS *fileSystem ) : _tft(tft), _fileSystem(fileSystem) { }; + ScreenShotService( LGFX *tft, fs::FS *fileSystem ) : _fileSystem(fileSystem) { setSource(tft); }; + ScreenShotService( LGFX_Sprite *sprite, fs::FS *fileSystem ) : _fileSystem(fileSystem) { setSource(sprite); }; ~ScreenShotService(); void init(); bool begin( bool ifPsram = true ); // ram test and allocation - /* + /*\ * M5.ScreenShot.snap() => /jpg/screenshot-YYYY-MM-DD_HH-MM-SS.jpg * M5.ScreenShot.snap("my-namespace") => /jpg/my-namespace-YYYY-MM-DD_HH-MM-SS.jpg * M5.ScreenShot.snap("/path/to/my-file.jpg") => /path/to/my-file.jpg - */ + \*/ void snap( const char* name = "screenshot", bool displayAfter = false ); - void snapJPG( const char* name = "screenshot", bool displayAfter = false ); void snapBMP( const char* name = "screenshot", bool displayAfter = false ); void snapPNG( const char* name = "screenshot", bool displayAfter = false ); void snapGIF( const char* name = "screenshot", bool displayAfter = false ); - void snapQOI( const char* name = "screenshot", bool displayAfter = false ); + size_t snapQOI( const char* name = "screenshot", bool displayAfter = false ); + size_t snapQOI( Stream* stream ); + + #if defined HAS_JPEGENC + /*\ + * AVI_Params_t aviparams = new AVI_Params_t( &SD, "/out.avi", fps, use_buffer, use_index_file ); + * while( capturing ) { + * animate( &canvas ); // animate canvas + * if( params->ready ) M5.ScreenShot.snapAVI( aviparams ); // add frame + * } + * if( params->ready ) M5.ScreenShot.snapAVI( aviparams, true ); // finalize + \*/ + void snapAVI( AVI_Params_t *params, bool finalize=false ); + void snapJPG( const char* name = "screenshot", bool displayAfter = false ); + #else + #define TinyJPEG_DEPRECATE_MSG "AVI/JPG capture is disabled, include @bitbanks2's to enable it" + [[deprecated(TinyJPEG_DEPRECATE_MSG)]] + inline void snapAVI( void *params, bool finalize=false ) { log_n(TinyJPEG_DEPRECATE_MSG); }; + [[deprecated(TinyJPEG_DEPRECATE_MSG)]] + void snapJPG( const char* name = "screenshot", bool displayAfter = false ) { log_n(TinyJPEG_DEPRECATE_MSG); }; + #endif + + template + void setSource( GFX *src ) + { + _is_sprite = std::is_same::value; + _src = (LGFX*)src; + _w = src->width(); + _h = src->height(); + }; + void setFs( fs::FS *fileSystem ) { _fileSystem = fileSystem; }; void setWindow( uint32_t x=0, uint32_t y=0, uint32_t w=0, uint32_t h=0 ); bool readPixelSuccess = false; // result of tft pixel read test - bool jpegCapture = true; // default yes until tested, BMP capture will be used if not enough ram is available + bool jpegCapture = false; // default no (needs JPEGENC library, PNG capture is default + const char* getFileName() { return fileName; } private: - uint32_t _x=0, _y=0, _w=0, _h=0; + uint32_t _x=0, _y=0, _w=0, _h=0; // capture window bool _begun = false; // prevent begin() from being called more than once bool _inited = false; // prevent memory to be allocated more than once bool _psram = false; + bool _is_sprite = false; + + LGFX* _src; + fs::FS* _fileSystem; char fileName[255] = {0}; char folderName[32] = {0}; - LGFX* _tft; - fs::FS* _fileSystem; - void genFileName( const char* name, const char* extension ); void checkFolder( const char* path ); void snapAnimation(); bool displayCanReadPixels(); - JPEG_Encoder *JPEGEncoder = nullptr; - BMP_Encoder *BMPEncoder = nullptr; - PNG_Encoder *PNGEncoder = nullptr; - GIF_Encoder *GIFEncoder = nullptr; - QOI_Encoder *QOIEncoder = nullptr; - - uint8_t* rgbBuffer = NULL; // used for jpeg only, bmp has his own + uint8_t* screenShotBuffer = NULL; // used by AVI and JPG encoders + uint32_t screenShotBufSize = 0; }; // end class - -#include "./JPG/TinyJPEGEncoder.hpp" #include "./BMP/TinyBMPEncoder.hpp" -#include "./PNG/FatPNGEncoder.hpp" // requires miniz.c patched with TDEFL_LESS_MEMORY=1 +#include "./PNG/FatPNGEncoder.hpp" #include "./GIF/TinyGIFEncoder.hpp" #include "./QOI/QOIEncoder.hpp" +