From bf94cb72435ec4fddc7851f28af18acc0cce5c43 Mon Sep 17 00:00:00 2001 From: applecuckoo <113647417+applecuckoo@users.noreply.github.com> Date: Mon, 18 Nov 2024 01:53:18 +1300 Subject: [PATCH] patterns: Added new WebP and VGM patterns (#294) * README: fix square bracket * patterns: add WebP pattern * patterns/dds: add x-dds mimetype * patterns: add vgm pattern * patterns/vgm: remove old pointer * patterns/protobuf: fix field number handling * patterns/protobuf: add .pb file extension * patterns/uf2: updating the family IDs again * patterns/png: add cHRM and tIME chunks * patterns/png: whoops, old description snuck back in * new quantized-mesh pattern * add quantized-mesh to README, implement oct16 decoding --- README.md | 5 +- patterns/dds.hexpat | 1 + patterns/png.hexpat | 28 ++- patterns/protobuf.hexpat | 36 ++-- patterns/quantized-mesh.hexpat | 169 +++++++++++++++ patterns/uf2.hexpat | 10 + patterns/vgm.hexpat | 248 ++++++++++++++++++++++ patterns/webp.hexpat | 122 +++++++++++ tests/patterns/test_data/webp.hexpat.webp | Bin 0 -> 934 bytes 9 files changed, 604 insertions(+), 15 deletions(-) create mode 100644 patterns/quantized-mesh.hexpat create mode 100644 patterns/vgm.hexpat create mode 100644 patterns/webp.hexpat create mode 100644 tests/patterns/test_data/webp.hexpat.webp diff --git a/README.md b/README.md index 2acf9c32..4a6ff877 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ Everything will immediately show up in ImHex's Content Store and gets bundled wi |------|------|------|-------------| | 3DS | | [`patterns/3ds.hexpat`](patterns/3ds.hexpat) | Autodesk 3DS Max Model file | | 7Z | | [`patterns/7z.hexpat`](patterns/7z.hexpat) | 7z File Format | -| ADTS | | [`patterns/adts.hexpat`(patterns/adts.hexpat) | ADTS/AAC audio files | +| ADTS | | [`patterns/adts.hexpat`](patterns/adts.hexpat) | ADTS/AAC audio files | | AFE2 | | [`patterns/afe2.hexpat`](patterns/afe2.hexpat) | Nintendo Switch Atmosphère CFW Fatal Error log | | ANI | `application/x-navi-animation` | [`patterns/ani.hexpat`](patterns/ani.hexpat) | Windows Animated Cursor file | | AR | `application/x-archive` | [`patterns/ar.hexpat`](patterns/ar.hexpat) | Static library archive files | @@ -115,6 +115,7 @@ Everything will immediately show up in ImHex's Content Store and gets bundled wi | PYC | | [`patterns/pyc.hexpat`](patterns/pyc.hexpat) | Python bytecode files | | QBCL | | [`patterns/qbcl.hexpat`](patterns/qbcl.hexpat) | Qubicle voxel scene project file | | QOI | `image/qoi` | [`patterns/qoi.hexpat`](patterns/qoi.hexpat) | QOI image files | +| quantized-mesh | | [`patterns/quantized-mesh.hexpat`](patterns/quantized-mesh.hexpat) | Cesium quantized-mesh terrain | | RAS | `image/x-sun-raster` | [`patterns/ras.hexpat`](patterns/ras.hexpat) | RAS image files | | ReFS | | [`patterns/refs.hexpat`](patterns/refs.hexpat) | Microsoft Resilient File System | | RGBDS | | [`patterns/rgbds.hexpat`](patterns/rgbds.hexpat) | [RGBDS](https://rgbds.gbdev.io) object file format | @@ -136,10 +137,12 @@ Everything will immediately show up in ImHex's Content Store and gets bundled wi | UF2 | | [`patterns/uf2.hexpat`](patterns/uf2.hexpat) | [USB Flashing Format](https://github.com/microsoft/uf2) | | VBMeta | | [`patterns/vbmeta.hexpat`](patterns/vbmeta.hexpat) | Android VBMeta image | | VDF | | [`patterns/vdf.hexpat`](patterns/vdf.hexpat) | Binary Value Data Format (.vdf) files | +| VGM | | [`patterns/vgm.hexpat`](patterns/vgm.hexpat) | VGM (Video Game Music) sound log | | VHDX | | [`patterns/vhdx.hexpat`](patterns/vhdx.hexpat) | Microsoft Hyper-V Virtual Hard Disk format | | WAV | `audio/x-wav` | [`patterns/wav.hexpat`](patterns/wav.hexpat) | RIFF header, WAVE header, PCM header | | WAS | | [`patterns\was_oskasoftware.hexpat`](patterns\was_oskasoftware.hexpat) | Oska Software DeskMates WAS/WA3 (WAVE/MP3 Set) file | WAD | | [`patterns/wad.hexpat`](patterns/wad.hexpat) | DOOM WAD Archive | +| WebP | `image/webp` | [`patterns/webp.hexpat`](patterns/webp.hexpat) | Google WebP image | | XBEH | `audio/x-xbox-executable` | [`patterns/xbeh.hexpat`](patterns/xbeh.hexpat) | Xbox executable | | XCI | | [`patterns/xci.hexpat`](patterns/xci.hexpat) | Nintendo Switch XCI cartridge ROM | | XGT | | [`patterns/xgt.hexpat`](patterns/xgstexture.hexpat) | Exient XGS Engine Texture | diff --git a/patterns/dds.hexpat b/patterns/dds.hexpat index a7c42a58..9cc60d76 100644 --- a/patterns/dds.hexpat +++ b/patterns/dds.hexpat @@ -1,6 +1,7 @@ #pragma description DirectDraw Surface #pragma MIME image/vnd-ms.dds +#pragma MIME image/x-dds #pragma endian little enum DXGI_FORMAT : u32 { diff --git a/patterns/png.hexpat b/patterns/png.hexpat index 8ab0d8ae..ebd7122e 100644 --- a/patterns/png.hexpat +++ b/patterns/png.hexpat @@ -1,4 +1,4 @@ -#pragma description PNG image +#pragma description PNG image #pragma MIME image/png #pragma endian big @@ -121,6 +121,26 @@ struct palette_entry_t { u24 color; } [[inline]]; +struct chrm_t { + u32 white_point_x; + u32 white_point_y; + u32 red_x; + u32 red_y; + u32 green_x; + u32 green_y; + u32 blue_x; + u32 blue_y; +}; + +struct time_t { + u16 year; + u8 month; + u8 day; + u8 hour; + u8 minute; + u8 second; +}; + struct chunk_t { u32 length [[color("17BECF")]]; char name[4]; @@ -139,6 +159,8 @@ struct chunk_t { #define acTL_k "acTL" #define fdAT_k "fdAT" #define fcTL_k "fcTL" + #define cHRM_k "cHRM" + #define tIME_k "tIME" if (name == IHDR_k) { ihdr_t ihdr [[comment("Image Header chunk"), name("IHDR")]]; @@ -167,6 +189,10 @@ struct chunk_t { } else if (name == fdAT_k) { fdat_t fdat [[comment("Frame data chunk")]]; u8 data[length-sizeof(u32)]; + } else if (name == cHRM_k) { + chrm_t chrm; + } else if (name == tIME_k) { + time_t time; } else { u8 data[length]; } diff --git a/patterns/protobuf.hexpat b/patterns/protobuf.hexpat index ba401cc4..ce17cd7b 100644 --- a/patterns/protobuf.hexpat +++ b/patterns/protobuf.hexpat @@ -1,7 +1,8 @@ #pragma author WerWolv -#pragma description Google Protobuf +#pragma description Google Protobuf wire encoding (.pb) import std.core; +import std.io; import std.mem; import type.leb128; @@ -31,10 +32,19 @@ enum WireType : u8 { _32Bit = 5 }; -bitfield Key { - field_number : 5; - wire_type : 3; -} [[bitfield_order(std::core::BitfieldOrder::MostToLeastSignificant, 8)]]; +WireType wire_type; +u32 tag; +u32 field_number; + +struct Key { + type::uLEB128 keyDec; + field_number = u32(keyDec) >> 3; + wire_type = u32(keyDec) & 7; +}[[sealed, format("format_key")]]; + +fn format_key(Key keyObject) { + return std::format("{} with field number {}", wire_type, field_number); +}; union _64Bit { u64 fixed64; @@ -49,22 +59,22 @@ union _32Bit { }; struct LengthDelimited { - type::LEB128 length; + type::uLEB128 length; char data[length]; }; struct Entry { - Key key; + Key key; - if (key.wire_type == WireType::Varint) - type::LEB128 value; - else if (key.wire_type == WireType::_64Bit) + if (wire_type == WireType::Varint) + type::uLEB128 value; + else if (wire_type == WireType::_64Bit) _64Bit value; - else if (key.wire_type == WireType::LengthDelimited) + else if (wire_type == WireType::LengthDelimited) LengthDelimited value; - else if (key.wire_type == WireType::_32Bit) + else if (wire_type == WireType::_32Bit) _32Bit value; }; -Entry entries[while(!std::mem::eof())] @ 0x00; +Entry entries[while(!std::mem::eof())] @ 0x00; \ No newline at end of file diff --git a/patterns/quantized-mesh.hexpat b/patterns/quantized-mesh.hexpat new file mode 100644 index 00000000..f4b05c57 --- /dev/null +++ b/patterns/quantized-mesh.hexpat @@ -0,0 +1,169 @@ +#pragma author applecuckoo +#pragma description Cesium quantized-mesh terrain +#pragma endian little + +// based on https://github.com/CesiumGS/quantized-mesh +// potential improvements: figure out high water mark encoding for indices + +import std.math; +import std.io; + +u8 extensionCount; +extensionCount = 0; // NOTE: set this to the amount of extensions in your terrain. + +// ZigZag decoder based on protobuf.hexpat by WerWolv + +struct ZigZag16 { + u16 value; +} [[sealed, format("format_zigzag16")]]; + +fn format_zigzag16(ZigZag16 zigzag) { + return s16((s16(zigzag.value) << 1) ^ (s16(zigzag.value) >> 15)); +}; + +struct QuantizedMeshHeader { + double CenterX; + double CenterY; + double CenterZ; + + float MinimumHeight; + float MaximumHeight; + + double BoundingSphereCenterX; + double BoundingSphereCenterY; + double BoundingSphereCenterZ; + double BoundingSphereRadius; + + double HorizonOcclusionPointX; + double HorizonOcclusionPointY; + double HorizonOcclusionPointZ; +}; + +struct VertexData { + u32 vertexCount; + ZigZag16 u[vertexCount]; + ZigZag16 v[vertexCount]; + ZigZag16 height[vertexCount]; +}; + +struct IndexData16 { + u32 triangleCount; + u16 indices[triangleCount * 3]; +}; + +struct IndexData32 { + u32 triangleCount; + u32 indices[triangleCount * 3]; +}; + +struct EdgeIndices16 { + u32 westVertexCount; + u16 westIndices[westVertexCount]; + + u32 southVertexCount; + u16 southIndices[southVertexCount]; + + u32 eastVertexCount; + u16 eastIndices[eastVertexCount]; + + u32 northVertexCount; + u16 northIndices[northVertexCount]; +}; + +struct EdgeIndices32 { + u32 westVertexCount; + u32 westIndices[westVertexCount]; + + u32 southVertexCount; + u32 southIndices[southVertexCount]; + + u32 eastVertexCount; + u32 eastIndices[eastVertexCount]; + + u32 northVertexCount; + u32 northIndices[northVertexCount]; +}; + +enum ExtensionTypes : u8 { + OctEncodedVertexNormals = 0x1, + WaterMask, + Metadata = 0x4, +}; + +// Oct16 decode based on https://github.com/loicgasser/quantized-mesh-tile/blob/master/quantized_mesh_tile/utils.py + +fn signNotZero(float v) { + if (v < 0.0) + return -1.0; + else + return 1.0; +}; + +fn fromSnorm(u8 value) { + return float(std::math::clamp(value, 0.0, 255.0) / 255.0 * 2.0 - 1.0); +}; + +struct Oct16 { + u8 x; + u8 y; +}[[sealed, format("format_oct16")]]; + +fn format_oct16(Oct16 oct) { + float xOut; + float yOut; + float zOut; + + xOut = fromSnorm(oct.x); + yOut = fromSnorm(oct.y); + zOut = 1.0 - (std::math::abs(xOut) + std::math::abs(yOut)); + + if (zOut < 0.0) { + float oldX; + + oldX = xOut; + xOut = (1.0 - std::math::abs(yOut)) * signNotZero(oldX); + yOut = (1.0 - std::math::abs(oldX)) * signNotZero(yOut); + } + + return std::format("{}, {}, {}", xOut, yOut, zOut); +}; + +struct OctEncodedVertexNormals { + Oct16 xy[parent.parent.vertdata.vertexCount]; +}; + +struct WaterMask { + u8 mask[parent.extensionLength]; +}; + +struct Metadata { + u32 jsonLength; + char json[jsonLength]; +}; + +struct ExtensionHeader { + u8 extensionId; + u32 extensionLength; + match (extensionId) { + (ExtensionTypes::OctEncodedVertexNormals): OctEncodedVertexNormals octVertNormals; + (ExtensionTypes::WaterMask): WaterMask maskData; + (ExtensionTypes::Metadata): Metadata metadata; + } +}; + +struct QuantizedMesh { + QuantizedMeshHeader header; + VertexData vertdata; + + if (vertdata.vertexCount > 65536) { + IndexData32 indexdata; + EdgeIndices32 edgeindices; + } else { + IndexData16 indexdata; + EdgeIndices16 edgeindices; + } + + ExtensionHeader extensions[extensionCount]; +}; + +QuantizedMesh mesh @ 0x00; \ No newline at end of file diff --git a/patterns/uf2.hexpat b/patterns/uf2.hexpat index a710c8a4..c974cce9 100644 --- a/patterns/uf2.hexpat +++ b/patterns/uf2.hexpat @@ -98,6 +98,16 @@ enum UF2_FamilyID : u32 { NRF52832xxAB = 0x6f752678, AT32F415 = 0xa0c97b8e, CH32V = 0x699b62ec, + RA4M1 = 0x7be8976d, + RTL8710A = 0x9fffd543, + RTL8710B = 0x22e0d6fc, + RTL8720C = 0xe08f7564, + RTL8720D = 0x3379CFE2, + XR809 = 0x51e903a8, + BK7231U = 0x675a40b0, + BK7251 = 0x6a82cc42, + BK7231N = 0x7b3ef230, + BL602 = 0xde1270b7, }; fn formatTagType(UF2_TagType type) { diff --git a/patterns/vgm.hexpat b/patterns/vgm.hexpat new file mode 100644 index 00000000..5f8f59b2 --- /dev/null +++ b/patterns/vgm.hexpat @@ -0,0 +1,248 @@ +#pragma author applecuckoo +#pragma description VGM (Video Game Music) sound log + +#pragma endian little + +import type.magic; +import std.string; +import std.io; + +u32 versionValue; +u32 gd3TagPos; +u32 chpClkBase; +u32 chpVolBase; + +// note: the versionValue variable exists to help check which fields exist and which don't, otherwise the actual log data would show up as part of the header. + +bitfield VGMVersion { + bugfix : 4; + minor : 4; + major : 24; + + versionValue = major * 100 + minor * 10 + bugfix; + +} [[format("format_VGMVersion")]]; + +fn format_VGMVersion(auto version) { + return std::format("{}.{}{}", version.major, version.minor, version.bugfix); +}; + +bitfield SN76489Flags { + frequency : 1; + negateOutput : 1; + GameGearStereo : 1; + clockDiv : 1; + XNORNoiseMode : 1; + padding : 3; +}; + +bitfield AY8910Flags { + legacyOutput : 1; + singleOutput : 1; + discreteOutput : 1; + RAWOutput : 1; + YMclockDivLow : 1; + padding : 3; +}; + +bitfield OKIM6258Flags { + clockDiv : 2; + ADPCMsel : 1; + outputBitDepth : 1; + padding : 4; +}; + +bitfield K054539Flags { + reverseStereo : 1; + disableReverb : 1; + KeyOnUpdate : 1; + padding : 5; +}; + +enum AY8190Type : u8 { + AY8910, + AY8912, + AY8913, + AY8914, + YM2149 = 0x10, + YM3439, + YMZ284, + YMZ294, +}; + +enum C140Type : u8 { + C140_NamcoSystem2, + C140_NamcoSystem21, + ASIC219_NamcoNA, +}; + +struct Gd3 { + type::Magic<"Gd3 "> ident; + VGMVersion version; + u32 Gd3Length; + std::string::NullString16 trackNameEng; + std::string::NullString16 trackNameOriginal; + std::string::NullString16 gameNameEng; + std::string::NullString16 gameNameOriginal; + std::string::NullString16 sysNameEng; + std::string::NullString16 sysNameOriginal; + std::string::NullString16 trackAuthorEnglish; + std::string::NullString16 trackAuthorOriginal; + std::string::NullString16 gameReleaseDate; + std::string::NullString16 VGMConverter; + std::string::NullString16 Notes; +}; + +struct baseHeader { + type::Magic<"Vgm "> ident; + u32 eofOffset; + VGMVersion version; + u32 SN76489_clk; + u32 YM2413_clk; + gd3TagPos = $; + u32 Gd3Offset; + u32 sampleCount; + u32 loopOffset; + u32 loopSamples; +}; + +struct Header : baseHeader { + if (versionValue >= 101) { + u32 rate; + } if (versionValue >= 110) { + u16 SN76489_feedback; + u8 SN76489_shift_width; + } if (versionValue >= 151) { + SN76489Flags snflags; + } else { + padding[1]; + } if (versionValue >= 110) { + u32 YM2612_clk; + u32 YM2151_clk; + } if (versionValue >= 150) { + u32 VGMOffset; + } if (versionValue >= 151) { + u32 SegaPCM_clk; + u32 SegaPCM_reg; + u32 RF5C68_clk; + u32 YM2203_clk; + u32 YM2608_clk; + u32 YM2610_clk; + u32 YM3812_clk; + u32 YM3526_clk; + u32 Y8950_clk; + u32 YMF262_clk; + u32 YMF278B_clk; + u32 YMF271_clk; + u32 YMZ280B_clk; + u32 RF5C164_clk; + u32 PWM_clk; + u32 AY8910_clk; + AY8190Type AY8910_type; + AY8910Flags AY8910_flags; + u8 YM2203_flags; + u8 YM2608_flags; + } if (versionValue >= 160) { + u8 volumeMod; + padding[1]; + u8 loopBase; + } else { + padding[3]; + } if (versionValue >= 151) { + u8 loopMod; + } if (versionValue >= 161) { + u32 DMG_clk; + u32 APU_clk; + u32 MultiPCM_clk; + u32 uPD7759_clk; + u32 OKIM6258_clk; + OKIM6258Flags OKIM6258_flags; + K054539Flags K054539_flags; + C140Type C140_type; + padding[1]; + u32 OKIM6295_clk; + u32 K051649_clk; + u32 K054539_clk; + u32 HuC6280_clk; + u32 C140_clk; + u32 K053260_clk; + u32 Pokey_clk; + u32 QSound_clk; + } if (versionValue >= 171) { + u32 SCSP_clk; + } else { + padding[4]; + } if (versionValue >= 170) { + u32 extraHeaderOffset; + } if (versionValue >= 171) { + u32 WonderSwan_clk; + u32 VSU_clk; + u32 SAA1099_clk; + u32 ES5503_clk; + u32 ES5005_clk; + u8 ES5503_ch; + u8 ES5505_ch; + u8 C352_clockDiv; + padding[1]; + u32 X1_010_clk; + u32 C352_clk; + u32 GA20_clk; + } if (versionValue > 172) { + u32 Mikey_clk; + } +}; + +struct chpClkEntry { + u8 chpID; + u32 chpClk; +}; + +struct chpClkHeader { + u8 entryCount; + chpClkEntry entries[entryCount]; +}; + +bitfield chpVolume { + volume : 15; + absoluteRelative : 1; +}; + +struct chpVolEntry { + u8 chpID; + u8 flags; + chpVolume chpVol; +}; + +struct chpVolHeader { + u8 entryCount; + chpVolEntry entries[entryCount]; +}; + +struct ExtraHeader { + u32 headerSize; + chpClkBase = $; + u32 chpClkOffset; + chpVolBase = $; + u32 chpVolOffset; + + if (chpClkOffset > 0) { + $ = chpClkBase + chpClkOffset; + chpClkHeader chpClk; + } if (chpVolOffset > 0) { + $ = chpVolBase + chpVolOffset; + chpVolHeader chpVol; + } + +}; + +struct VGM { + Header header; + if (versionValue >= 170) { + if (header.extraHeaderOffset > 0) { + ExtraHeader extraHeader; + } + } + Gd3 tag @ (gd3TagPos + header.Gd3Offset); +}; + +VGM vgm @ 0x00; diff --git a/patterns/webp.hexpat b/patterns/webp.hexpat new file mode 100644 index 00000000..611b76b2 --- /dev/null +++ b/patterns/webp.hexpat @@ -0,0 +1,122 @@ +#pragma author applecuckoo +#pragma description Google WebP + +#pragma endian little +#pragma MIME image/webp + +// based off of ttf.hexpat by Rebuild and wav.hexpat by WerWolv + +import std.mem; +import std.core; +import type.magic; + +struct RiffHeader { + char ckID[4] [[comment("Container Signature"), name("RIFF Header Signature")]]; + u32 ckSize [[comment("Size of RIFF Header"), name("RIFF Chunk Size")]]; + char format[4] [[comment("RIFF format"), name("WAVE Header Signature")]]; +}; + +struct WebPChunk { + char chunkId[4]; + u32 chunkSize; +}; + +bitfield VP8XFlags { + padding : 1; + A : 1; + X : 1; + E : 1; + L : 1; + I : 1; + padding : 2; +}; + +struct OneBase { + u24 value; +} [[sealed, format("format_onebase")]]; + +fn format_onebase(OneBase onebase) { + return onebase.value + 1; +}; + +struct WebPVP8XData { + VP8XFlags flags; + padding[3]; + OneBase canvasWidth; + OneBase canvasHeight; +}; + +struct WebPANIMData { + u32 backgroundColor; + u16 loopCount; +}; + +bitfield ANMFFlags { + D : 1; + B : 1; + padding : 6; +}; + +bitfield WebPVP8LHeader { + widthMinusOne : 14; + heightMinusOne : 14; + alphaUsed : 1; + version : 3; +}; + +struct WebPVP8LData { + type::Magic<"\x2f"> id; + WebPVP8LHeader header; +}; + +u32 frameSize; + +struct WebPANMFData { + u24 frameX; + u24 frameY; + OneBase frameWidth; + OneBase frameHeight; + u24 frameDuration; + ANMFFlags flags; + u8 data[parent.chunkHeader.chunkSize - 16]; // lazy fix - can't be bothered implementing subchunks + +}; + +bitfield ALPHFlags { + C : 2; + F : 2; + P : 2; + padding : 2; +}; + +u32 paddedChunkSize; + +struct WebPData { + WebPChunk chunkHeader; + paddedChunkSize = (chunkHeader.chunkSize + 1) >> 1 << 1; + match (chunkHeader.chunkId) { + ("VP8X"): WebPVP8XData VP8XData; + ("ANIM"): WebPANIMData ANIMData; + ("ANMF"): { + WebPANMFData ANMFData; + padding[paddedChunkSize - sizeof(ANMFData)]; + } + ("VP8L"): { + WebPVP8LHeader VP8LData; + u8 image[chunkHeader.chunkSize-3]; + padding[paddedChunkSize - sizeof(VP8LData) - sizeof(image)]; + } + ("ALPH"): { + ALPHFlags flags; + u8 ALPHData[chunkHeader.chunkSize-1]; + padding[paddedChunkSize - sizeof(ALPHData) - sizeof(flags)]; + } + (_): { + u8 data[chunkHeader.chunkSize]; + padding[paddedChunkSize - sizeof(data)]; + } + } +} [[name(std::format("Chunk ({})", chunkHeader.chunkId))]];; + +RiffHeader header @0x00; +WebPData data[while (!std::mem::eof())] @ $; diff --git a/tests/patterns/test_data/webp.hexpat.webp b/tests/patterns/test_data/webp.hexpat.webp new file mode 100644 index 0000000000000000000000000000000000000000..510916df98e539f0f6d1738291b5acf11eb93789 GIT binary patch literal 934 zcmV;X16ll1Nk&GV0{{S5MM6+kP&il$0000G0000#002J#06|PpNM`{600EG+|DPc_ z^{;<*uWj4*?rCqfcVpYOlatMBp4^OTt-28=LagP5!opv5GP_3B4S_&(Qu*R*|Qkj<|e_YJ7n` z23>5#d(h4R$-%kx4UPB-Dz=@3>_sP%@d~t65!s>fNy+#QN|yq5pyM{4g*K89#Fp*y zI!X|@9UUP&9c>U_lG|%2-bI@Wfv2JM4z69q{YRo59hG=G+Tab;fsXs)*=Qq~Bh`;i zX5iImn?=T?{{(b>BR-FI1+gjOkeNv9!3aghLgyMe68#G#c&3qq5yph{A8%x5kQ5$X zMlpa?^!z1Dmo8nVap}^fi*haUq-vUTpTNYnkFR!Hk?=+`Dt>YS09H^qAP@oo0MHBo zodGI906+jfg*=rXY&tFk3r_7lqRO=9v&%*a=yhVQ~_+c>tL!TLf}NuOtRe2%m!fQ1Kk zaf7znUwh*rT0MGseFl|OG$a!3f)R%DM52ok*>zn578~n|K60H0hS)Bx?HSqIMxHT7 zyVy(P)4MPH&2^OUJB@h~8d>}0@2vV%#0;fAV&f_By%a30|8faUw^1B-Kmri=c94i9 zXYw(XzD3%{Ma@px?HBRV}d}0}YCgpjmNoMk3R