GeoKMZer

geoKMZer is a JavaScript library designed to convert KMZ into KML files, use with GeoKMLer to convert to GeoJSON.

此脚本不应直接安装,它是一个供其他脚本使用的外部库。如果您需要使用该库,请在脚本元属性加入:// @require https://update.cn-greasyfork.org/scripts/527113/1538395/GeoKMZer.js

// ==UserScript==
// @name                GeoKMZer
// @namespace           https://github.com/JS55CT
// @description         geoKMZer is a JavaScript library designed to convert KMZ into KML files, use with GeoKMLer to convert to GeoJSON.
// @version             1.1.0
// @author              JS55CT
// @license             MIT
// @match              *://this-library-is-not-supposed-to-run.com/*
// ==/UserScript==

/***********************************************************
 * ## Project Home < https://github.com/JS55CT/GeoKMLer >
 *  MIT License
 * Copyright (c) 2025 Justin
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), 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.
 **************************************************************/
var GeoKMZer = (function () {
  /**
   * GeoKMZer constructor function, which optionally wraps an object.
   * @param {Object} [obj] - Optional object to wrap.
   * @returns {GeoKMZer} - An instance of GeoKMZer.
   */
  function GeoKMZer(obj) {
    if (obj instanceof GeoKMZer) return obj;
    if (!(this instanceof GeoKMZer)) return new GeoKMZer(obj);
    this._wrapped = obj; // Optional: wrap any input object if needed
  }

  /**
   * Converts a buffer of various types to a Uint8Array.
   * @param {ArrayBuffer|TypedArray} buffer - The buffer to convert.
   * @returns {Uint8Array} - The converted Uint8Array.
   * @throws Will throw an error if the buffer is not a valid buffer-like object.
   */
  function toUint8Array(buffer) {
    if (!buffer) {
      throw new Error("forgot to pass buffer");
    }
    if (ArrayBuffer.isView(buffer)) {
      // Buffer is a typed array view like Uint8Array
      return new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
    }
    if (buffer instanceof ArrayBuffer) {
      // Buffer is an ArrayBuffer
      return new Uint8Array(buffer);
    }
    throw new Error("invalid buffer like object");
  }

  /**
   * Yields entries from a ZIP archive contained in a buffer.
   * @generator
   * @param {Uint8Array} buffer - The buffer representing the ZIP file.
   * @yields {Object} - An object containing filename, comment, and a read() method to get file content.
   */
  GeoKMZer.prototype.parseZipEntries = function* (buffer) {
    const textDecoder = new TextDecoder();

    const decodeText = (buffer) => textDecoder.decode(buffer);

    const findEndOfCentralDirectory = (buffer) => {
      let offset = buffer.length - 20;
      const minSearchOffset = Math.max(offset - 65516, 2);
      while ((offset = buffer.lastIndexOf(80, offset - 1)) !== -1 && !(buffer[offset + 1] === 75 && buffer[offset + 2] === 5 && buffer[offset + 3] === 6) && offset > minSearchOffset);
      return offset;
    };

    const throwError = (message) => {
      throw new Error("unzip-error: " + message);
    };

    // Declare the decompression handling function
    let decompressWithDecompressionStream;
    const compressionFormat = "deflate-raw";

    try {
      new self.DecompressionStream(compressionFormat);
      decompressWithDecompressionStream = async (compressedData) => {
        const decompressionStream = new self.DecompressionStream(compressionFormat);
        const writer = decompressionStream.writable.getWriter();
        const reader = decompressionStream.readable.getReader();

        writer.write(compressedData);
        writer.close();

        const decompressedChunks = [];
        let totalLength = 0;
        let position = 0;
        let readResult;

        while (!(readResult = await reader.read()).done) {
          const chunk = readResult.value;
          decompressedChunks.push(chunk);
          totalLength += chunk.length;
        }

        if (decompressedChunks.length > 1) {
          const combinedArray = new Uint8Array(totalLength);
          for (const chunk of decompressedChunks) {
            combinedArray.set(chunk, position);
            position += chunk.length;
          }
          return combinedArray;
        } else {
          return decompressedChunks[0];
        }
      };
    } catch {
      console.error("DecompressionStream is unsupported or initialization failed.");
    }

    let centralDirectoryEnd = findEndOfCentralDirectory(buffer);

    if (centralDirectoryEnd === -1) {
      throwError(2);
    }

    const subarray = (start, length) => buffer.subarray((centralDirectoryEnd += start), (centralDirectoryEnd += length));
    const dataView = new DataView(buffer.buffer, buffer.byteOffset);
    const getUint16 = (offset) => dataView.getUint16(offset + centralDirectoryEnd, true);
    const getUint32 = (offset) => dataView.getUint32(offset + centralDirectoryEnd, true);

    let numberOfEntries = getUint16(10);

    if (numberOfEntries !== getUint16(8)) {
      throwError(3);
    }

    centralDirectoryEnd = getUint32(16);

    while (numberOfEntries--) {
      let compressionType = getUint16(10),
        filenameLength = getUint16(28),
        extraFieldLength = getUint16(30),
        fileCommentLength = getUint16(32),
        compressedSize = getUint32(20),
        localHeaderOffset = getUint32(42),
        filename = decodeText(subarray(46, filenameLength)),
        comment = decodeText(subarray(extraFieldLength, fileCommentLength)),
        previousCentralDirectoryEnd = centralDirectoryEnd,
        compressedData;

      centralDirectoryEnd = localHeaderOffset;
      compressedData = subarray(30 + getUint16(26) + getUint16(28), compressedSize);

      yield {
        filename,
        comment,
        read: () => {
          if (compressionType & 8) {
            return decompressWithDecompressionStream(compressedData);
          } else if (compressionType) {
            throwError(1);
          } else {
            return compressedData;
          }
        },
      };

      centralDirectoryEnd = previousCentralDirectoryEnd;
    }
  };

  /**
   * Unzips a KMZ buffer, potentially recursively, and retrieves contained KML files.
   * @param {ArrayBuffer|TypedArray} buffer - The buffer of the KMZ file.
   * @param {string} [parentFile=''] - Name of the parent file if dealing with nested KMZ files.
   * @returns {Object} - An object containing file names and their corresponding data buffers.
   * @throws Will throw an error if no KML files are found.
   */
  GeoKMZer.prototype.unzipKMZ = async function (buffer, parentFile = "") {
    const files = {};
    const kmlFileRegex = /.+\.kml$/i;
    const kmzFileRegex = /.+\.kmz$/i;
    const uint8Buffer = toUint8Array(buffer);

    for (const entry of this.parseZipEntries(uint8Buffer)) {
      if (kmlFileRegex.test(entry.filename)) {
        files[entry.filename] = await entry.read();
      } else if (kmzFileRegex.test(entry.filename)) {
        // Handle nested KMZ file
        try {
          const nestedKMZBuffer = await entry.read();
          const nestedFiles = await this.unzipKMZ(nestedKMZBuffer, entry.filename);
          Object.assign(files, nestedFiles); // Merge files found in nested archives
        } catch (nestedError) {
          console.error(`Error reading nested KMZ file "${entry.filename}":`, nestedError);
        }
      } 
    }

    if (Object.keys(files).length === 0) {
      throw new Error("No KML file found in the KMZ archive.");
    }

    return files;
  };

  /**
   * Reads a KMZ buffer and extracts KML files into an array of textual contents.
   * @param {ArrayBuffer|TypedArray} buffer - The buffer of the KMZ file.
   * @returns {Array} - An array of objects, each containing the filename and content of a KML file.
   * @throws Will log errors if any occur during KMZ reading.
   */
  GeoKMZer.prototype.read = async function (buffer) {
    try {
      const kmlFiles = await this.unzipKMZ(buffer);
      const textDecoder = new TextDecoder();
      const kmlContentsArray = [];

      for (const [kmlFilename, kmlBuffer] of Object.entries(kmlFiles)) {
        const kmlContent = textDecoder.decode(kmlBuffer); // Decode the KML buffer to text
        kmlContentsArray.push({ filename: kmlFilename, content: kmlContent }); // Store each content with its filename
      }

      return kmlContentsArray;
    } catch (error) {
      console.error("Error during KMZ reading:", error);
    }
  };

  return GeoKMZer;
})();