diff --git a/pixinsight_update_bundle/doc/docs/Notification Event Script for WBPP/Notification Event Script for WBPP.html b/pixinsight_update_bundle/doc/docs/Notification Event Script for WBPP/Notification Event Script for WBPP.html new file mode 100644 index 0000000..21c6976 --- /dev/null +++ b/pixinsight_update_bundle/doc/docs/Notification Event Script for WBPP/Notification Event Script for WBPP.html @@ -0,0 +1,201 @@ + + + + + PixInsight Reference Documentation | WBPP Notification Event Script + + + + + + + + + + + + + + + +

WBPP Notification Event Script

+
+

By Luca Bartek

+
+ +
+ +
+

A PixInsight script for sending notifications at key events in the WBPP pipeline. [more]

+ +
+

Keywords: WBPP, Telegram, iMessage, Pushover, Notifications, PixInsight, Pipeline, Automation

+
+ +

Contents

+

[hide]

+
+ +
+ +
+ +
+

1 Introduction

+

[hide]

+
+

The Notification Event Script provides a way to receive real-time notifications during the execution of the Weighted Batch Preprocessing (WBPP) pipeline in PixInsight. This script supports multiple messaging platforms, including Telegram, iMessage, and Pushover, to send updates, error messages, and image notifications.

+

Support for Telegram text-only notifications was originally implemented by Marco Manenti. See his project at https://github.com/verbavolant/eventSendMessageToTelegram.

+

Support for Pushover notifications with images was contributed by Brian Weber, whose guide can be found at https://blog.briangweber.com/pushover-setup/.

+
+
+ +
+

2 Setup

+

[hide]

+
+

In order to use the Notification Event Script with WBPP (Weighted Batch Preprocessing), you first need to configure the notification system you want to use. This is done through the SendMessageToPhone script, which currently allows you to select either Telegram, iMessage, or Pushover as the platform to receive notifications.

+

Follow the steps below to configure the notification system:

+

Step 1: Open the SendMessageToPhone script in PixInsight's Script Explorer.

+

Step 2: Select the messaging platform you want to use: Telegram, iMessage, or Pushover. You will see three radio buttons: one for Telegram, one for iMessage, and one for Pushover. Click the radio button next to the platform you want to use.

+

Step 3a: If you selected Telegram, you will have to enter the Telegram Bot Token and Chat ID. To send notifications via Telegram, you'll need to Set up your Telegram Bot first. This feature was originally implemented by Marco Manenti, and a standalone version of his script can be found at https://github.com/verbavolant/eventSendMessageToTelegram.

+

Step 3b: If you selected iMessage, enter the Phone Number of the recipient in international format. For iMessage, enter the recipient of the person who will receive the notifications. This is likely your own Apple ID if you'd like to receive the notifications yourself, but technically you could insert any AppleID here. Ensure PixInsight has the rights to execute AppleScript: Run the script standalone once, at which point it will ask for the necessary permissions. Accept.

+

Step 3c: If you selected Pushover, enter your User Key and App Token. To use Pushover, you must create an account at https://pushover.net and register a new application. Copy the credentials and paste them into the appropriate fields. Pushover works across all operating systems and does not rely on a specific platform. Setup instructions and examples are available in Brian Weber’s guide at https://blog.briangweber.com/pushover-setup/.

+

Step 4: Save the configuration by clicking the Save Parameters button. Once you've filled out the necessary fields, click the Save Parameters button to save your settings. This will store the Bot Token, Chat ID, recipient phone number, and other options so that the script can send notifications during the WBPP process.

+ +

An example of a Telegram Bot Configuration

+
+
+ +
+

3 Usage

+

[hide]

+
+

Notifications typically include:

+ +
    +
  • Current WBPP step
  • +
  • Operation status summary
  • +
  • Error message (in case of failure)
  • +
+ +

Example messages:

+ +
WBPP started
+ + +
ImageIntegration starting: processing 48/62 active frames
+ + +
LocalNormalization failed, Cannot open reference image
+ + +
WBPP terminated
+ + +
ImageIntegration successfully executed
+ +
+
+ +
+

4 Setting up a Telegram Bot

+

[hide]

+
+

Setting Up a Telegram Bot

+ +
    +
  • On your phone, open Telegram and search for the user BotFather.
  • +
  • Start a chat and type: newbot.
  • +
  • Follow the instructions to create a bot and receive an API token e.g. 63xxxxxx71:AAFoxxxxn0hwA-2TVSxxxNf4c.
  • +
  • Copy the bot token; you will need it for this script.
  • +
+ +

Finding Your Telegram Chat ID

+ +
    +
  • Start a new chat with your new bot.
  • +
  • Visit the website https://api.telegram.org/bot63xxxxxx71:AAFoxxxxn0hwA-2TVSxxxNf4c/getUpdates - make sure to replace the bot token with the token you got from the BotFather.
  • +
  • It will show you a response similar to the following:
  • +
+ +

"ok": true,
+ "result": [
+ {
+ "update_id": 83xxxxx35,
+ "message": {
+ "message_id": 2643,
+ "from": {...},
+ "chat": {
+ "id": 123456789,
+ "first_name": "...",
+ "last_name": "...",
+ "username": "@username",
+ "type": "private"
+ },
+ "date": 1703062972,
+ "text": "/start"
+ } } ]} +

    +
  • The number you get in the chat section in the id field is your chat id.
  • +
+ +

+
+
+ +
+

5 Limitations

+

[hide]

+
+ +
    +
  • The script uses the curl utility to interact with Telegram and Pushover. Ensure your system has access to the internet and curl is available.
  • +
  • iMessage sending is only available on macOS. If you're running this script on Windows or Linux, use Telegram or Pushover instead.
  • +
  • Pushover imposes a limit of 2.5MB on image attachments. If your image is too large, the script will downsample it. Only JPEG format is supported.
  • +
  • Notifications may be delayed if there is network congestion or if the bot/token/user credentials are incorrect.
  • +
+ +
+
+ +
+

6 Troubleshooting

+

[hide]

+
+ +
    +
  • If no notifications are received, ensure the Telegram Bot Token and Chat ID, iMessage Recipient, or Pushover credentials are entered correctly.
  • +
  • Check if the curl utility is working properly on your system. You can test this by sending a test message manually using the command line.
  • +
  • If using Pushover, ensure that the image is smaller than 2.5MB, and that it is being saved as a valid JPEG.
  • +
  • If you receive a "No valid target image found" error, ensure there is an active image in PixInsight when running the script.
  • +
+ +
+
+ +
+ + + + +
+
+ +
+ + + diff --git a/pixinsight_update_bundle/doc/docs/Notification Event Script for WBPP/images/configuring_telegramBot.png b/pixinsight_update_bundle/doc/docs/Notification Event Script for WBPP/images/configuring_telegramBot.png new file mode 100644 index 0000000..c93f331 Binary files /dev/null and b/pixinsight_update_bundle/doc/docs/Notification Event Script for WBPP/images/configuring_telegramBot.png differ diff --git a/pixinsight_update_bundle/doc/docs/WBPP No Code Pipeline Builder/WBPP No Code Pipeline Builder.html b/pixinsight_update_bundle/doc/docs/WBPP No Code Pipeline Builder/WBPP No Code Pipeline Builder.html new file mode 100644 index 0000000..12d65ee --- /dev/null +++ b/pixinsight_update_bundle/doc/docs/WBPP No Code Pipeline Builder/WBPP No Code Pipeline Builder.html @@ -0,0 +1,227 @@ + + + + + PixInsight Reference Documentation | WBPP No Code Pipeline Builder + + + + + + + + + + + + + + + +

WBPP No Code Pipeline Builder

+
+

By Luca Bartek, Roberto Sartori

+
+ +
+ +
+

An extension for WBPP that enables fully customizable pipeline execution using a ProcessContainer object. [more]

+ +
+

Keywords: WBPP, ProcessContainer, Automation, Script Execution, PixInsight, Pipeline Builder

+
+ +

Contents

+

[hide]

+
+ +
+ +
+ +
+

1 Introduction

+

[hide]

+
+

This extension allows users to define the complete WBPP pipeline through a standard PixInsight ProcessContainer, which acts as a programmable execution list. Each process inside the container is dynamically assigned to a WBPP pipeline stage.

+

The WBPP No Code Pipeline Builder is designed for users who want to extend WBPP without coding. It leverages a hierarchy of key=value metadata tags to define when and on which groups a process should run, without editing the WBPP core logic.

+

This method offers full control over the execution order and group targeting while making the pipeline fully visible and editable using PixInsight's graphical tools.

+
+
+ +
+

2 How It Works

+

[hide]

+
+

The user creates a ProcessContainer instance. Inside this process container, any number of processes can be added, including other nested ProcessContainer elements.

+

Processes will only be executed if at least the step tag is present either on the process level or at a parent level (e.g. on the main process container).

+

Each element must declare its intent through a step tag and optional filtering metadata like cam, filter, binning, etc. - any of the user defined keywords from the WBPP instance.

+

Once happy with the process, the user saves the ProcessContainer instance as a process icon (by dragging the triangle of the ProcessContainer to the PixInsight desktop. This icon must then be renamed tp WBPP - either by clicking the letter N in the top right corner of the process icon, or by right-clicking the process icon and selecting Set Icon Identifier.

+

Please note that once the icon is created for the Process Container instance, its content may not be modified. If a modification is needed, double-click the Process Icon, do the required changes and save it as a new process icon by dragging the blue triangle to the desktop.

+

Only one process icon may be present with the identifier WBPP - the old one must be renamed or deleted.

+

To find out more about ProcessContainers, click here: Legacy ProcessContainer documentation.

+

Key concepts of the No Code Pipeline Builder:

+ +
    +
  • Processes are applied only if their declared step matches the current WBPP pipeline stage.
  • +
  • Each process is applied only to groups that match all declared metadata keys.
  • +
  • Steps and metadata can be inherited from the container they are placed in. Overriding is supported.
  • +
  • The special key step determines when the process is invoked.
  • +
  • Additional metadata keys define filtering constraints.
  • +
+ +

Processes are applied to each matching group and master variant (regular, drizzle, autocrop) in sequence.

+
+
+ +
+

3 Creating the WBPP Container

+

[hide]

+
+ +
    +
  • Create a ProcessContainer and name it WBPP.
  • +
  • Insert child processes or nested ProcessContainers.
  • +
  • Add metadata to define steps and filtering logic. Use the Description field for key=value tags.
  • +
  • Example: step=onPostProcessEnd,cam=ASI183MM will apply only to groups with that camera at the final step.
  • +
+ +

Metadata placed on a container will be inherited by all its children, unless a child overrides it.

+ +

Keyword hierarchy/inheritance rules

+
+
+ +
+

4 Available Step Tags

+

[hide]

+
+

These are the valid step values:

+ +
    +
  • onCalibrationEnd — after calibration on PRE groups with active calibration
  • +
  • onPreProcessEnd — always called at end of PRE block
  • +
  • onPostProcessStart — always called before POST block starts
  • +
  • onRegistrationStart — if registration is enabled
  • +
  • onRegistrationEnd — when registration finishes
  • +
  • onIntegrationStart — before integration
  • +
  • onPostProcessEnd — always called at the end of POST block - applied on master files
  • +
+ +

Wherever it is not specified that the step is always executed, steps are conditional based on whether the related step is enabled in WBPP.

+
+
+ +
+

5 Key-Value Filtering

+

[hide]

+
+

In addition to step, any number of custom keys can be added to filter matching groups. Examples include:

+ +
    +
  • cam=ASI183MM — matches only frames from this camera
  • +
  • night=1 — matches frames from the first night
  • +
+ +

A process will be applied only to groups that satisfy all specified conditions. Please note that the above examples are referring to keywords defined by the user in the WBPP Keywords section.

+
+
+ +
+

6 Hierarchy and Inheritance

+

[hide]

+
+ +
    +
  • Keys declared on a ProcessContainer apply to all children unless overridden.
  • +
  • Multiple keys must be comma-separated in a single line: step=onIntegrationStart,cam=ASI183MM
  • +
  • If the same key appears again deeper in the structure, the inner value takes precedence.
  • +
+ +

This makes it easy to apply the same filter to multiple operations while retaining flexibility for exceptions.

+
+
+ +
+

7 Example

+

[hide]

+
+ +

An example of a WBPP ProcessContainer defining multiple steps with filtering tags.

+

A container with multiple elements:

+ +
    +
  • ProcessContainer "WBPP" — no keywords defined
  • +
  • IntegerResamplestep=onCalibrationEnd
  • +
  • ProcessContainer - contains steps to be performed on the masters — step=onPostProcessEnd
  • +
  • BlurXTerminator — inherits step from parent
  • +
  • NoiseXTerminator — inherits step from parent
  • +
  • Script - in this case an autostretch — inherits step from parent
  • +
+ +

This will apply resampling on every single sub after the calibration step has finished. It will then apply BlurXTerminator, NoiseXTerminator and an autostretch on every master frame generated.

+
+
+ +
+

8 Use Cases

+

[hide]

+
+

Examples of practical applications:

+ +
    +
  • Apply a series of basic processing steps to the final stacks to get a first idea of the quality of the obtained data and have a quick look.
  • +
  • Downsample calibrated frames by applying IntegerResample
  • +
  • Apply a linear AutomaticBackgroundExtraction on the calibrated images prior to registering
  • +
+ +
+
+ +
+

9 Limitations

+

[hide]

+
+ +
    +
  • WBPP must be executed with the ProcessContainer WBPP present in the workspace.
  • +
  • If the step name is invalid or mistyped, the operation is silently skipped.
  • +
  • The script has only been tested with a subset of PixInsight processes. Some processes may have unexpected results. If you think a process should reasonably be supported, please signal this to the developer at info<at>thespacekoala<dot>com - specifying which keywords were used on which levels, what the expected result vs. the actual result is. Screen shots and logs are most helpful.
  • +
  • Processes which have the option of generating a new image or overwriting the target image should be use with the latter configuration. The original master files are never overwritten.
  • +
  • No graphical UI is provided yet for defining the key-value structure.
  • +
  • Does not support conditional branching or complex logic — only filtering.
  • +
  • Scripts cannot be included in the container because PixInsight does not allow script instantiation in that execution context. Only standard process icons are supported.
  • +
+ +
+
+ +
+ + + + +
+
+ +
+ + + diff --git a/pixinsight_update_bundle/doc/docs/WBPP No Code Pipeline Builder/images/wbpp_hierarchy_rules.png b/pixinsight_update_bundle/doc/docs/WBPP No Code Pipeline Builder/images/wbpp_hierarchy_rules.png new file mode 100644 index 0000000..a7e60c5 Binary files /dev/null and b/pixinsight_update_bundle/doc/docs/WBPP No Code Pipeline Builder/images/wbpp_hierarchy_rules.png differ diff --git a/pixinsight_update_bundle/doc/docs/WBPP No Code Pipeline Builder/images/wbpp_processcontainer_screenshot.png b/pixinsight_update_bundle/doc/docs/WBPP No Code Pipeline Builder/images/wbpp_processcontainer_screenshot.png new file mode 100644 index 0000000..cbf7411 Binary files /dev/null and b/pixinsight_update_bundle/doc/docs/WBPP No Code Pipeline Builder/images/wbpp_processcontainer_screenshot.png differ diff --git a/pixinsight_update_bundle/doc/docs/WBPP Telegram Notification Event Script/WBPP Telegram Notification Event Script.html b/pixinsight_update_bundle/doc/docs/WBPP Telegram Notification Event Script/WBPP Telegram Notification Event Script.html new file mode 100644 index 0000000..70b5934 --- /dev/null +++ b/pixinsight_update_bundle/doc/docs/WBPP Telegram Notification Event Script/WBPP Telegram Notification Event Script.html @@ -0,0 +1,202 @@ + + + + + PixInsight Reference Documentation | WBPP Telegram Notifications + + + + + + + + + + + + + + + +

WBPP Telegram Notifications

+
+

By Luca Bartek, Marco Manenti

+
+ +
+ +
+

Adds Telegram bot integration to WBPP pipelines, sending real-time status messages and images at key execution points. [more]

+ +
+

Keywords: WBPP, Telegram, Notifications, PixInsight, Pipeline, Automation

+
+ +

Contents

+

[hide]

+
+ +
+ +
+ +
+

1 Introduction

+

[hide]

+
+

This extension adds Telegram notification support to the Weighted Batch Preprocessing (WBPP) process. Notifications inform the user about pipeline progress, completion, and errors in real time via Telegram messages. This is particularly helpful for long unattended processing tasks or remote system operation.

+
+
+ +
+

2 Setup

+

[hide]

+
+

To use Telegram notifications, follow these steps:

+ +
    +
  • Open the Pipeline tab in WBPP.
  • +
  • Add an Event Script. Use the script +
    NotificationEventScript.js
    + +, available in the TheSpaceKoala PixInsight repository. After downloading the scripts, you'll find it together in the PixInsight scripts directory.
  • +
  • Open the script in a text editor. Modify the following two lines at the top:
  • +
  • const TELEGRAM_BOT_TOKEN = 'your-token-here';
  • +
  • const TELEGRAM_CHAT_ID = 'your-chat-id-here';
  • +
  • Save the script with these changes.
  • +
  • It is recommended that you duplicate the script (e.g., MyTelegramEventScript.js) into another location and use that instead to avoid overwriting your settings during updates.
  • +
+ +

Setting Up a Telegram Bot

+ +
    +
  • Open Telegram and search for BotFather.
  • +
  • Start a chat and type: /newbot.
  • +
  • Follow the instructions to create a bot and receive an API token, e.g. 63xxxxxx71:AAFoxxxxn0hwA-2TVSxxxNf4c.
  • +
  • Copy the bot token; you will need it for the script.
  • +
+ +

Finding Your Telegram Chat ID

+ +
    +
  • Start a new chat with your newly created bot.
  • +
  • Visit the website: https://api.telegram.org/bot/getUpdates (replace with the actual token from BotFather).
  • +
  • You will receive a JSON response similar to this:
  • +
+ +

"ok": true,
+ "result": [
+ {
+ "update_id": 83xxxxx35,
+ "message": {
+ "message_id": 2643,
+ "from": {...},
+ "chat": {
+ "id": 123456789,
+ "first_name": "...",
+ "last_name": "...",
+ "username": "@username",
+ "type": "private"
+ },
+ "date": 1703062972,
+ "text": "/start"
+ } } ]}

+ +
    +
  • The number you get in the chat section in the id field is your Telegram Chat ID.
  • +
+ +
+
+ +
+

3 Message Types

+

[hide]

+
+

The script supports the following notification types:

+ +
    +
  • Start Notification — Sent when the WBPP pipeline begins.
  • +
  • Step Notifications — Sent when each WBPP step starts or completes.
  • +
  • Error Notification — Sent immediately when an error occurs.
  • +
  • Completion Notification — Sent when the pipeline finishes successfully.
  • +
  • Image Notification — A JPEG preview of the final stacked image is downsampled, autostretched, and sent via Telegram.
  • +
+ +
+
+ +
+

4 Message Content

+

[hide]

+
+

Notifications typically include:

+ +
    +
  • Current WBPP step
  • +
  • Operation status summary
  • +
  • Error message (in case of failure)
  • +
+ +

Example messages:

+ +
WBPP started
+ + +
ImageIntegration starting: processing 48/62 active frames
+ + +
LocalNormalization failed, Cannot open reference image
+ + +
WBPP terminated
+ + +
ImageIntegration successfully executed
+ +
+
+ +
+

5 Limitations

+

[hide]

+
+ +
    +
  • Telegram integration requires external network access and uses the curl utility.
  • +
  • If the bot token or chat ID is invalid, no message is sent.
  • +
  • Messages are silently skipped if configuration is missing.
  • +
+ +

Important Notice: The previous Telegram notification script is now deprecated. We recommend switching to the new

+ +
NotificationEventScript.js
+ +

, which is more flexible and powerful, offering support for multiple platforms and advanced configuration options.

+
+
+ +
+ + + + +
+
+ +
+ + + diff --git a/pixinsight_update_bundle/doc/pidoc/css/pidoc-common.css b/pixinsight_update_bundle/doc/pidoc/css/pidoc-common.css new file mode 100644 index 0000000..85bcc6a --- /dev/null +++ b/pixinsight_update_bundle/doc/pidoc/css/pidoc-common.css @@ -0,0 +1,863 @@ +/* + * PixInsight Reference Documentation System + * + * Common style sheet for all document classes + * Updated 2023 March 8 + * + * Copyright (c) 2010-2023 Pleiades Astrophoto S.L. All Rights Reserved. + */ + +/* -------------------------------------------------------------------------- */ + +/* + * Open Sans + */ + +@font-face { + font-family: 'OpenSans'; + src: url('../../../rsc/fonts/OpenSans/ttf/OpenSans-Light.ttf') format('truetype'); + font-weight: 300; + font-style: normal; +} + +@font-face { + font-family: 'OpenSans'; + src: url('../../../rsc/fonts/OpenSans/ttf/OpenSans-LightItalic.ttf') format('truetype'); + font-weight: 300; + font-style: italic; +} + +@font-face { + font-family: 'OpenSans'; + src: url('../../../rsc/fonts/OpenSans/ttf/OpenSans-Regular.ttf') format('truetype'); + font-weight: normal; + font-style: normal; +} + +@font-face { + font-family: 'OpenSans'; + src: url('../../../rsc/fonts/OpenSans/ttf/OpenSans-Italic.ttf') format('truetype'); + font-weight: normal; + font-style: italic; +} + +@font-face { + font-family: 'OpenSans'; + src: url('../../../rsc/fonts/OpenSans/ttf/OpenSans-Semibold.ttf') format('truetype'); + font-weight: 500; + font-style: normal; +} + +@font-face { + font-family: 'OpenSans'; + src: url('../../../rsc/fonts/OpenSans/ttf/OpenSans-SemiboldItalic.ttf') format('truetype'); + font-weight: 500; + font-style: italic; +} + +@font-face { + font-family: 'OpenSans'; + src: url('../../../rsc/fonts/OpenSans/ttf/OpenSans-Bold.ttf') format('truetype'); + font-weight: bold; + font-style: normal; +} + +@font-face { + font-family: 'OpenSans'; + src: url('../../../rsc/fonts/OpenSans/ttf/OpenSans-BoldItalic.ttf') format('truetype'); + font-weight: bold; + font-style: italic; +} + +@font-face { + font-family: 'OpenSans'; + src: url('../../../rsc/fonts/OpenSans/ttf/OpenSans-ExtraBold.ttf') format('truetype'); + font-weight: 800; + font-style: normal; +} + +@font-face { + font-family: 'OpenSans'; + src: url('../../../rsc/fonts/OpenSans/ttf/OpenSans-ExtraBoldItalic.ttf') format('truetype'); + font-weight: 800; + font-style: italic; +} + +/* -------------------------------------------------------------------------- */ + +/* + * DejaVu Sans + */ + +@font-face { + font-family: 'DejaVu Sans'; + src: url('../../../rsc/fonts/DejaVu/ttf/DejaVuSans.ttf') format('truetype'); + font-weight: normal; + font-style: normal; +} + +@font-face { + font-family: 'DejaVu Sans'; + src: url('../../../rsc/fonts/DejaVu/ttf/DejaVuSans-Oblique.ttf') format('truetype'); + font-weight: normal; + font-style: italic; +} + +@font-face { + font-family: 'DejaVu Sans'; + src: url('../../../rsc/fonts/DejaVu/ttf/DejaVuSans-Bold.ttf') format('truetype'); + font-weight: bold; + font-style: normal; +} + +@font-face { + font-family: 'DejaVu Sans'; + src: url('../../../rsc/fonts/DejaVu/ttf/DejaVuSans-BoldOblique.ttf') format('truetype'); + font-weight: bold; + font-style: italic; +} + +@font-face { + font-family: 'DejaVu Sans'; + src: url('../../../rsc/fonts/DejaVu/ttf/DejaVuSans-ExtraLight.ttf') format('truetype'); + font-weight: 300; + font-style: normal; +} + +/* -------------------------------------------------------------------------- */ + +/* + * Hack + */ + +@font-face { + font-family: 'Hack'; + src: url('../../../rsc/fonts/Hack/ttf/Hack-Regular.ttf') format('truetype'); + font-weight: normal; + font-style: normal; +} + +@font-face { + font-family: 'Hack'; + src: url('../../../rsc/fonts/Hack/ttf/Hack-Italic.ttf') format('truetype'); + font-weight: normal; + font-style: italic; +} + +@font-face { + font-family: 'Hack'; + src: url('../../../rsc/fonts/Hack/ttf/Hack-Bold.ttf') format('truetype'); + font-weight: bold; + font-style: normal; +} + +@font-face { + font-family: 'Hack'; + src: url('../../../rsc/fonts/Hack/ttf/Hack-BoldItalic.ttf') format('truetype'); + font-weight: bold; + font-style: italic; +} + +/* -------------------------------------------------------------------------- */ + +/* + * DejaVu Sans Mono + */ + +@font-face { + font-family: 'DejaVu Sans Mono'; + src: url('../../../rsc/fonts/DejaVu/ttf/DejaVuSansMono.ttf') format('truetype'); + font-weight: normal; + font-style: normal; +} + +@font-face { + font-family: 'DejaVu Sans Mono'; + src: url('../../../rsc/fonts/DejaVu/ttf/DejaVuSansMono-Oblique.ttf') format('truetype'); + font-weight: normal; + font-style: italic; +} + +@font-face { + font-family: 'DejaVu Sans Mono'; + src: url('../../../rsc/fonts/DejaVu/ttf/DejaVuSansMono-Bold.ttf') format('truetype'); + font-weight: bold; + font-style: normal; +} + +@font-face { + font-family: 'DejaVu Sans Mono'; + src: url('../../../rsc/fonts/DejaVu/ttf/DejaVuSansMono-BoldOblique.ttf') format('truetype'); + font-weight: bold; + font-style: italic; +} + +/* -------------------------------------------------------------------------- */ + +:root { + --block-highlight-0: transparent; + --block-highlight-1: rgba(255,128,0,0.1); + --block-highlight-2: rgba(255,128,0,0.2); + --block-highlight-3: rgba(255,128,0,0.3); + --block-highlight-4: rgba(255,128,0,0.4); + --block-highlight-5: rgba(255,128,0,0.5); +} + +body { + margin: 0px; + padding: 1em; + padding-left: 80px; + width: 850px; + height: 100%; + font-family: OpenSans,"DejaVu Sans",Verdana,Arial,Helvetica,sans-serif; + font-size: 10.5pt; + font-weight: normal; + line-height: 1.25em; + color: #000000; + background: #ffffff; + background-position: top left; + background-attachment: fixed; + background-repeat: no-repeat; + background-image: url( '../../pidoc/titles/pixinsight_reference_documentation_title_vert.en.svg' ); + background-size: 40px auto; +} + +a { + text-decoration: none; + color: #3333ff; +} +a:visited { +} +a:active { +} +a:hover { + text-decoration: underline; +} + +strong, b { + font-weight: bold; +} +strong strong { + font-weight: normal; +} + +em, i { + font-style: italic; +} +em em { + font-style: normal; +} + +p { + margin-top: 1em; + margin-bottom: 1em; +} + +ul, ol { + position: relative; + left: 1em; + padding-left: 1em; + margin-top: 1em; + margin-bottom: 1em; + list-style-position: outside; +} + +ul { + list-style-type: square; +} + +li { + margin-top: 0.25em; + margin-bottom: 0.25em; +} +.pidoc_spaced_list_item { + padding-top: 1em; +} + +dl dl { + margin-left: 2em; +} +dt { + font-weight: bold; +} +dd { + margin-left: 2em; + margin-top: 0.4em; + margin-bottom: 1em; +} + +h1 { + font-family: OpenSans,"DejaVu Sans",Verdana,Arial,Helvetica,sans-serif; + font-size: 28pt; + font-weight: 300; /* light */ + line-height: 1.2em; + letter-spacing: -1px; + margin-top: 1em; + margin-bottom: 0.5em; + color: #0066ff; +} +h2 { + font-family: OpenSans,"DejaVu Sans",Verdana,Arial,Helvetica,sans-serif; + font-size: 20pt; + font-weight: 400; /* regular */ + line-height: 1.2em; + letter-spacing: -1px; + margin-top: 1.5em; + color: #0066ff; +} +h3 { + font-family: OpenSans,"DejaVu Sans",Verdana,Arial,Helvetica,sans-serif; + font-size: 18pt; + font-weight: 400; /* regular */ + line-height: 1.2em; + letter-spacing: -0.5px; + color: #0066ff; +} +h4 { + font-family: OpenSans,"DejaVu Sans",Verdana,Arial,Helvetica,sans-serif; + font-size: 14pt; + font-weight: 500; /* semi-bold */ + line-height: 1.2em; + color: #990000; +} +h5 { + font-family: OpenSans,"DejaVu Sans",Verdana,Arial,Helvetica,sans-serif; + font-size: 12pt; + font-weight: 500; /* semi-bold */ + line-height: 1.2em; + color: #660000; +} + +@media print { + body { + font-size: 12pt; + } + h1 { + font-size: 30pt; + } + h2 { + font-size: 22pt; + } + h3 { + font-size: 20 + } + h4 { + font-size: 16pt; + } + h5 { + font-size: 14pt; + } +} + +img { + border: none; + display: inline; + margin: 0; + padding: 0; +} +img.block { + display: block; +} + +hr { + border: none; + background: #969696; + color: #969696; + width: 100%; + height: 2px; +} +hr.separator { + margin-top: 2em; + margin-bottom: 2em; + background: #d0d0d0; + color: #d0d0d0; +} + +table { + border-collapse: collapse; +} +tr:nth-child(odd) { + background-color: #ebebeb; +} +tr:nth-child(even) { + background-color: #fbfbfb; +} +caption { + color: #ffffff; + background-color: #a0a0a0; + border-top: 1px solid #a0a0a0; + border-left: 1px solid #a0a0a0; + border-right: 1px solid #a0a0a0; + border-bottom: 1px solid #eeeeee; + font-size: 90%; + font-style: normal; + text-align: center; + padding: 0.15em; +} +th { + color: #ffffff; + background-color: #a0a0a0; + border: 1px solid #eeeeee; + border-top: 1px solid #a0a0a0; + font-size: 90%; + text-align: left; + padding-top: 0.30em; + padding-bottom: 0.30em; + padding-left: 0.55em; + padding-right: 0.55em; +} +th:first-child { + border-left: 1px solid #a0a0a0; +} +th:last-child { + border-right: 1px solid #a0a0a0; +} +td { + border: 1px solid #a0a0a0; + padding-top: 0.50em; + padding-bottom: 0.50em; + padding-left: 0.55em; + padding-right: 0.55em; +} + +.pidoc_table { + margin-top: 1em; + margin-bottom: 1em; +} +.pidoc_table p { + display: block; +} +.pidoc_table p:nth-child(1), .pidoc_table p:first-of-type { + margin-top: 0; + padding-top: 0; +} +.pidoc_table p:nth-last-child(1), .pidoc_table p:last-of-type { + margin-bottom: 0; + padding-bottom: 0; +} +.pidoc_table_title { + font-weight: bold; +} + +.pidoc_math { + font-family: "DejaVu Serif","Times New Roman",Times,serif; +} + +.pidoc_note { + color: #007000; + font-style: italic; +} +.pidoc_note em { + font-style: normal; +} + +.pidoc_section { +} + +.pidoc_subsection { +} + +.pidoc_group { +} + +.pidoc_figure { + border: 2px solid #cccccc; + margin: 0px; + padding: 8px; + background-color: #eeeeee; + font-size: 80%; + margin-top: 1em; + margin-bottom: 1em; +} +.pidoc_figure p { + display: block; +} +.pidoc_figure p:nth-child(1), .pidoc_figure p:first-of-type { + margin-top: 0; +} +.pidoc_figure p:nth-last-child(1), .pidoc_figure p:last-of-type { + margin-bottom: 0; +} +.pidoc_figure img { + display: inline; +} +.pidoc_figure li { + padding-right: 8px; +} +.pidoc_figure_title { + font-weight: bold; +} + +.pidoc_box { + border: 2px solid #cccccc; + margin: 0px; + padding: 8px; + font-size: 90%; + margin-top: 1em; + margin-bottom: 1em; +} +.pidoc_box p { + display: block; +} +.pidoc_box p:nth-child(1), .pidoc_box p:first-of-type { + margin-top: 0; +} +.pidoc_box p:nth-last-child(1), .pidoc_box p:last-of-type { + margin-bottom: 0; +} +.pidoc_box img { + display: inline; +} +.pidoc_box li { + padding-right: 8px; +} + +.pidoc_referenceTooltip { + display: inline; + position: relative; + text-decoration: none; + box-shadow: 2px 4px 2px rgba(0,0,0,0.3); + -webkit-box-shadow: 2px 4px 2px rgba(0,0,0,0.3); +} +.pidoc_referenceTooltip:hover { + text-decoration: none; + box-shadow: none; + -webkit-box-shadow: none; +} +.pidoc_referenceTooltipWindow { + position: fixed; + left: 0; + top: 0; + max-width: 300px; + z-index: -1; + opacity: 0; /* initially hidden, to be shown dynamically */ + background: rgba(245,245,255,0.9); + border: 1px solid gray; + font-size: 90%; + color: black; + content: ''; + padding: 10px; + box-shadow: 4px 8px 4px rgba(0,0,0,0.3); + -webkit-box-shadow: 4px 8px 4px rgba(0,0,0,0.3); + transition: opacity 0.25s; + /* transitions cause problems with QtWebKit as of Qt 4.8.6 */ + /*-webkit-transition: opacity 0.25s;*/ +} +.pidoc_referenceTooltipWindow img { /* tooltip equations */ + margin-top: 0.5em; + max-width: 100%; + height: auto; +} + +.pidoc_mouseover { +} +.pidoc_mouseover p { + margin-top: 0; + margin-bottom: 0; + font-size: 80%; +} +.pidoc_mouseover ul { + list-style-type: none; + font-size: 80%; +} +.pidoc_mouseover li { + margin-top: 0.1em; + margin-bottom: 0.1em; +} +.pidoc_mouseover a { + cursor: default; + text-decoration: none; +} +.pidoc_mouseover .pidoc_image_left { + float: right; + margin-left: 0.75em; +} +.pidoc_mouseover .pidoc_image_right { + float: left; + margin-right: 0.75em; +} +.pidoc_figure .pidoc_mouseover p { + font-size: 100%; +} +.pidoc_figure .pidoc_mouseover ul { + font-size: 100%; +} +.pidoc_box .pidoc_mouseover p { + font-size: 100%; +} +.pidoc_box .pidoc_mouseover ul { + font-size: 100%; +} +.pidoc_indicator, .pidoc_indicator_default { + display: inline-block; + vertical-align: bottom; + margin-bottom: 0.1em; +} +.pidoc_indicator:after, .pidoc_indicator_default:after { + content: "\25B6\00A0"; +} +.pidoc_indicator { + opacity: 0.0; +} +.pidoc_indicator_default { + opacity: 1.0; +} + +/* Clearfix */ +.pidoc_clearfix, +.pidoc_section:after, +.pidoc_subsection:after, +.pidoc_group:after, +.pidoc_figure:after, +.pidoc_box:after, +.pidoc_mouseover img:after { + content: ''; + display: table; + clear: both; +} + +/* \hs */ +.pidoc_hspacer:before { + content: ' '; + display: inline; + visibility: hidden; +} +/* \vs */ +.pidoc_vspacer:before { + content: ' '; + display: block; + visibility: hidden; +} + +kbd, .pidoc_menu { + font-family: "Hack","DejaVu Sans Mono","Courier New",Courier,monospace; + font-size: 95%; + font-weight: bold; + color: #800000; +} + +pre, code, .pidoc_argument { + font-family: "Hack","DejaVu Sans Mono","Courier New",Courier,monospace; + font-size: 95%; + white-space: pre; +} +pre.code { + border: 2px dashed #cccccc; + margin: 1em 0px 1em 0px; + padding: 4px 8px 4px 8px; +} + +.pidoc_argument { + font-style: italic; +} + +.pidoc_code { + font-family: "Hack","DejaVu Sans Mono","Courier New",Courier,monospace; + font-size: 90%; +} + +.pidoc_equation { + text-align: center; +} + +.pidoc_equation_number { + float: right; +} + +.pidoc_wrap { + white-space: normal; + /*text-wrap:normal;*/ +} +.pidoc_nowrap { + white-space: nowrap; + /*text-wrap:none;*/ +} + +/* + * Document Sections + */ + +.pidoc_tocItem { + font-weight: bold; + padding-top: 1.0em; +} +.pidoc_tocSubitem { + font-weight: normal; + margin-left: 2em; +} + +.pidoc_sectionTitle { + clear: both; + border-bottom: 1px solid #999999; + padding-bottom: 0.1em; + margin-top: 1.75em; + margin-bottom: 1.25em; +} + +.pidoc_subsectionTitle { + clear: both; + margin-top: 1.5em; + margin-bottom: 0.7em; +} + +.pidoc_sectionToggleButton { + font-size: 90%; + text-align: right; + margin-top: -3.8em; + margin-bottom: 2.5em; + cursor: ns-resize; +} + +#authors { + clear: both; +} + +#brief { + clear: both; +} + +#categories { + clear: both; +} + +#keywords { + clear: both; +} + +#toc { + clear: both; + font-size: 85%; +} +#toc ul li { + display: block; + list-style-type: none; +} +#toc li { + margin-top: 0px; + margin-bottom: 0px; +} +#toc li:first-child { + padding-top: 0; +} + +#abstract { + clear: both; +} + +#acknowledgments { + clear: both; +} + +#references { + clear: both; +} + +#relatedTools { + clear: both; +} + +#relatedResources { + clear: both; +} + +#copyright { + clear: both; +} + +#footer { + clear: both; + font-size: 95%; +} + +/* + * Figures + */ +.figure { + border: 2px solid #cccccc; + margin: 0px; + padding: 8px; + background-color: #eeeeee; +} +.figure_panel { + border-top: 2px solid #cccccc; + border-bottom: 2px solid #cccccc; + padding-top: 8px; + padding-bottom: 8px; + background-color: #eeeeee; +} +.figure_caption { + font-size: 80%; +} +.figure_credits { + font-size: 75%; +} +.figure_padded { + padding-left: 1em; + padding-right: 1em; +} + +/* + * Miscellaneous + */ + +.identifier { + font-family: "Hack","DejaVu Sans Mono","Courier New",Courier,monospace; +} +.menu_item { + font-style: italic; +} +.shortcut { + font-family: "Hack","DejaVu Sans Mono","Courier New",Courier,monospace; +} + +.small { + font-size: 80%; +} +.tiny { + font-size: 75%; +} + +.red { + color: #990000; +} +.green { + color: #006600; +} +.blue { + color: #0000ff; +} +.invisible { + display: none; +} + +.courier { + font-family: "Courier New",Courier; +} +.symbol { + font-family: symbol; +} +.helvetica { + font-family: Arial,Helvetica,sans-serif; +} + +.firstLine { + margin-top: 0px; +} + +/* + * Collapsible sections + */ + +.collapsible { + margin-top: 4px; + margin-bottom: 0.5em; + background: #f0f0f0; + border: 1px solid #d0d0d0; + padding-left: 1em; + padding-right: 1em; +} + +.collapsibleLink { + font-weight: bold; + margin-bottom: 0.25em; +} diff --git a/pixinsight_update_bundle/doc/pidoc/css/pidoc-generic.css b/pixinsight_update_bundle/doc/pidoc/css/pidoc-generic.css new file mode 100644 index 0000000..a0c8647 --- /dev/null +++ b/pixinsight_update_bundle/doc/pidoc/css/pidoc-generic.css @@ -0,0 +1,8 @@ +/* + * PixInsight Reference Documentation System + * + * Style sheet for the generic documentation class + * Updated 2017 June 21 + * + * Copyright (c) 2010-2017 Pleiades Astrophoto S.L. All Rights Reserved. + */ diff --git a/pixinsight_update_bundle/doc/pidoc/css/pidoc-highlight.css b/pixinsight_update_bundle/doc/pidoc/css/pidoc-highlight.css new file mode 100644 index 0000000..ae09a6a --- /dev/null +++ b/pixinsight_update_bundle/doc/pidoc/css/pidoc-highlight.css @@ -0,0 +1,248 @@ +/* + * PixInsight Reference Documentation System + * + * Style sheet for syntax highlighting of \code blocks. + * Updated 2022 March 12 + * + * Copyright (c) 2010-2022 Pleiades Astrophoto S.L. All Rights Reserved. + */ + +.pidoc_sh_keyword { + color: inherit; + background: transparent; + font-weight: bold; + font-style: normal; + text-decoration: none; +} + +.pidoc_sh_ecmaKeyword { + color: #708090; + background: transparent; + font-weight: bold; + font-style: normal; + text-decoration: none; +} + +.pidoc_sh_pclKeyword { + color: inherit; + background: transparent; + font-weight: bold; + font-style: normal; + text-decoration: none; +} + +.pidoc_sh_qtKeyword { + color: #00a0ff; + background: transparent; + font-weight: bold; + font-style: normal; + text-decoration: none; +} + +.pidoc_sh_object { + color: #0000cd; + background: transparent; + font-weight: bold; + font-style: normal; + text-decoration: none; +} + +.pidoc_sh_externalObject { + color: #6b8e23; + background: transparent; + font-weight: bold; + font-style: normal; + text-decoration: none; +} + +.pidoc_sh_preprocessor { + color: #008000; + background: transparent; + font-weight: normal; + font-style: normal; + text-decoration: none; +} + +.pidoc_sh_pclMacro { + color: #9200ff; + background: transparent; + font-weight: normal; + font-style: normal; + text-decoration: none; +} + +.pidoc_sh_function { + color: #00505a; + background: transparent; + font-weight: normal; + font-style: normal; + text-decoration: none; +} + +.pidoc_sh_number { + color: #dc143c; + background: transparent; + font-weight: normal; + font-style: normal; + text-decoration: none; +} + +.pidoc_sh_hexNumber { + color: #dc143c; + background: transparent; + font-weight: normal; + font-style: normal; + text-decoration: none; +} + +.pidoc_sh_octNumber { + color: #dc143c; + background: transparent; + font-weight: normal; + font-style: normal; + text-decoration: none; +} + +.pidoc_sh_regExp { + color: #845a28; + background: transparent; + font-weight: normal; + font-style: normal; + text-decoration: none; +} + +.pidoc_sh_quotation { + color: #a52a2a; + background: transparent; + font-weight: normal; + font-style: normal; + text-decoration: none; +} + +.pidoc_sh_singleLineComment { + color: #6a5acd; + background: transparent; + font-weight: normal; + font-style: italic; + text-decoration: none; +} + +.pidoc_sh_multiLineComment { + color: #6a5acd; + background: transparent; + font-weight: normal; + font-style: italic; + text-decoration: none; +} + +.pidoc_sh_alert { + color: #ff0000; + background: #c8ffc8; + font-weight: bold; + font-style: normal; + text-decoration: none; +} + +.pidoc_sh_pidocCommand { + color: #800000; + background: transparent; + font-weight: bold; + font-style: normal; + text-decoration: none; +} + +.pidoc_sh_pidocSpecialCommand { + color: #ff0000; + background: #00ff00; + font-weight: normal; + font-style: normal; + text-decoration: none; +} + +.pidoc_sh_pidocReference { + color: #0000ff; + background: #faeaa0; + font-weight: normal; + font-style: normal; + text-decoration: none; +} + +.pidoc_sh_pidocBlockDelimiter { + color: inherit; + background: transparent; + font-weight: bold; + font-style: normal; + text-decoration: none; +} + +.pidoc_sh_pidocCharacterEntity { + color: #0000ff; + background: #e0ffff; + font-weight: normal; + font-style: normal; + text-decoration: none; +} + +.pidoc_sh_pidocParameters { + color: inherit; + background: transparent; + font-weight: normal; + font-style: italic; + text-decoration: none; +} + +.pidoc_sh_pidocVerbatimBlock { + color: #008000; + background: transparent; + font-weight: normal; + font-style: normal; + text-decoration: none; +} + +.pidoc_sh_pidocComment { + color: #6a5acd; + background: transparent; + font-weight: normal; + font-style: italic; + text-decoration: none; +} + +.pidoc_sh_xmlTag { + color: inherit; + background: transparent; + font-weight: bold; + font-style: normal; + text-decoration: none; +} + +.pidoc_sh_xmlAttributeName { + color: #0000cd; + background: transparent; + font-weight: bold; + font-style: normal; + text-decoration: none; +} + +.pidoc_sh_xmlAttributeValue { + color: #a52a2a; + background: transparent; + font-weight: normal; + font-style: normal; + text-decoration: none; +} + +.pidoc_sh_xmlEntity { + color: #008000; + background: transparent; + font-weight: normal; + font-style: normal; + text-decoration: none; +} + +.pidoc_sh_xmlCDATA { + color: #c71586; + background: transparent; + font-weight: normal; + font-style: normal; + text-decoration: none; +} diff --git a/pixinsight_update_bundle/doc/pidoc/css/pidoc-pjsr-auto.css b/pixinsight_update_bundle/doc/pidoc/css/pidoc-pjsr-auto.css new file mode 100644 index 0000000..aba0657 --- /dev/null +++ b/pixinsight_update_bundle/doc/pidoc/css/pidoc-pjsr-auto.css @@ -0,0 +1,100 @@ +/* + * PixInsight Reference Documentation System + * + * Style sheet for automatically generated PJSR documentation + * Updated 2023 March 8 + * + * Copyright (c) 2010-2023 Pleiades Astrophoto S.L. All Rights Reserved. + */ + +/* + * Hack Font + */ + +@font-face { + font-family: 'Hack'; + src: url('../../../rsc/fonts/Hack/ttf/Hack-Regular.ttf') format('truetype'); + font-weight: normal; + font-style: normal; +} + +@font-face { + font-family: 'Hack'; + src: url('../../../rsc/fonts/Hack/ttf/Hack-Italic.ttf') format('truetype'); + font-weight: normal; + font-style: italic; +} + +@font-face { + font-family: 'Hack'; + src: url('../../../rsc/fonts/Hack/ttf/Hack-Bold.ttf') format('truetype'); + font-weight: bold; + font-style: normal; +} + +@font-face { + font-family: 'Hack'; + src: url('../../../rsc/fonts/Hack/ttf/Hack-BoldItalic.ttf') format('truetype'); + font-weight: bold; + font-style: italic; +} + +/* -------------------------------------------------------------------------- */ + +body { + margin: 0; + padding: 1em; + font-family: "Hack","DejaVu Sans Mono","Courier New",Courier,monospace; + font-size: 9pt; +} + +table { + width: 100%; + margin-bottom: 1.5em; + border-collapse: collapse; +} +table:nth-last { + margin-bottom: 0; +} + +th, td { + text-align: left; + white-space: normal; + padding: 0.5em; + text-indent: -2em; + padding-left: 2.5em; + border: 1px solid #a0a0a0; +} +th { + background-color: #dcf0f8; + font-size: 10pt; + font-weight: bold; +} +#properties th { + background-color: rgba( 255, 169, 153, .40 ) +} +#static-properties th { + background-color: rgba( 255, 253, 153, .40 ) +} +#event-handlers th { + background-color: rgba( 204, 255, 153, .40 ) +} +#constructors th { + background-color: rgba( 207, 192, 255, .40 ) +} +#methods th { + background-color: rgba( 154, 227, 255, .40 ) +} +#static-methods th { + background-color: rgba( 154, 255, 245, .40 ) +} +#constants th { + background-color: rgba( 255, 202, 127, .40 ) +} + +tr:nth-child(even) { + background-color: rgba( 0, 0, 0, .03 ) +} +tr:nth-child(odd) { + background-color: rgba( 0, 0, 0, .10 ) +} diff --git a/pixinsight_update_bundle/doc/pidoc/css/pidoc-pjsr.css b/pixinsight_update_bundle/doc/pidoc/css/pidoc-pjsr.css new file mode 100644 index 0000000..f11e66f --- /dev/null +++ b/pixinsight_update_bundle/doc/pidoc/css/pidoc-pjsr.css @@ -0,0 +1,80 @@ +/* + * PixInsight Reference Documentation System + * + * Style sheet for the PIJSObjectDoc documentation class + * Updated 2017 June 21 + * + * Copyright (c) 2010-2017 Pleiades Astrophoto S.L. All Rights Reserved. + */ + +body { + background-position: top left; + background-attachment: fixed; + background-repeat: no-repeat; + background-image: url( '../../pidoc/titles/pixinsight_javascript_runtime_documentation_title_vert.en.svg' ); + background-size: 40px auto; +} + +.pidoc_objectItem { + background: #f0f0f0; + border: 2px solid #e0e0e0; + margin-top: 1em; + margin-bottom: 1em; +} +.pidoc_objectItem tr:nth-child(odd) { + background-color: #e4e4e4; +} +.pidoc_objectItem tr:nth-child(even) { + background-color: #ebebeb; +} +.pidoc_objectItem td { + padding: 0.7em; +} + +.pidoc_objectFormalDescription { + font-weight: bold; + color: #404AAA; + padding: 0.5em; + margin-top: 0.5em; +} + +.pidoc_objectDescription { + padding-left: 2em; + padding-right: 1em; +} +.pidoc_objectDescription table { /* parameters */ + width: 100%; +} +.pidoc_objectDescription td:last-child { /* parameter description */ + width: 100%; +} + +.pidoc_readOnlyProperty { +} + +.pidoc_readOnlyPropertyTag { + font-size: 85%; + font-weight: normal; + color: #A03C46; +} + +.pidoc_objectToc { + border-collapse: collapse; + background: transparent; +} +.pidoc_objectToc tr { + background: transparent; +} +.pidoc_objectToc td { + background: transparent; + border: none; + line-height: 1.22em; + padding: 0.50em; +} +.pidoc_objectToc p { + display: block; +} +.pidoc_objectTocType { + text-align: right; + min-width: 10em; +} diff --git a/pixinsight_update_bundle/doc/pidoc/css/pidoc-project.css b/pixinsight_update_bundle/doc/pidoc/css/pidoc-project.css new file mode 100644 index 0000000..686cddb --- /dev/null +++ b/pixinsight_update_bundle/doc/pidoc/css/pidoc-project.css @@ -0,0 +1,16 @@ +/* + * PixInsight Reference Documentation System + * + * Style sheet for the PIProjectDoc documentation class + * Updated 2023 March 13 + * + * Copyright (c) 2010-2023 Pleiades Astrophoto S.L. All Rights Reserved. + */ + +body { + background-position: top left; + background-attachment: fixed; + background-repeat: no-repeat; + background-image: url( '../../pidoc/titles/pixinsight_project_documentation_title_vert.en.svg' ); + background-size: 40px auto; +} diff --git a/pixinsight_update_bundle/doc/pidoc/css/pidoc-tool.css b/pixinsight_update_bundle/doc/pidoc/css/pidoc-tool.css new file mode 100644 index 0000000..24ff2c3 --- /dev/null +++ b/pixinsight_update_bundle/doc/pidoc/css/pidoc-tool.css @@ -0,0 +1,14 @@ +/* + * PixInsight Reference Documentation System + * + * Style sheet for the tool documentation class + * Updated 2017 June 21 + * + * Copyright (c) 2010-2017 Pleiades Astrophoto S.L. All Rights Reserved. + */ + +.pidoc_parameter { +} + +.pidoc_parameterTitle { +} diff --git a/pixinsight_update_bundle/doc/pidoc/icons/pidoc-icon.svg b/pixinsight_update_bundle/doc/pidoc/icons/pidoc-icon.svg new file mode 100644 index 0000000..c2081ff --- /dev/null +++ b/pixinsight_update_bundle/doc/pidoc/icons/pidoc-icon.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pixinsight_update_bundle/doc/pidoc/scripts/pidoc-utility.js b/pixinsight_update_bundle/doc/pidoc/scripts/pidoc-utility.js new file mode 100644 index 0000000..4a06989 --- /dev/null +++ b/pixinsight_update_bundle/doc/pidoc/scripts/pidoc-utility.js @@ -0,0 +1,145 @@ +/* + * PixInsight Reference Documentation System + * + * Utility JavaScript routines + * Updated 2013 October 3 + * + * Copyright (c) 2010-2013 Pleiades Astrophoto S.L. All Rights Reserved. + */ + +/* + * Returns an array with the x,y pixel position of an object on the current document. + */ +function pidoc_findPos( obj ) +{ + var dx = 0; + var dy = 0; + while ( obj ) + { + dx += obj.offsetLeft; + dy += obj.offsetTop; + obj = obj.offsetParent; + } + return new Array( dx, dy ); +} + +/* + * Toggles the style.display CSS property of an object with id. + */ +function pidoc_toggleDisplay( id ) +{ + var obj = document.getElementById( id ); + if ( obj ) + obj.style.display = obj.style.display ? "" : "none"; +} + +function pidoc_toggleSection( sectionId, button ) +{ + var obj = document.getElementById( sectionId ); + if ( obj ) + { + var wasVisible = obj.style.display.length == 0; + obj.style.display = wasVisible ? "none" : ""; + button.innerHTML = wasVisible ? "[show]" : "[hide]"; + } +} + +/* + * Sets the 'src' attribute of an image selected by its id. + */ +function pidoc_setImgSrc( imgId, imgSrc ) +{ + var obj = document.getElementById( imgId ); + if ( obj ) + obj.src = imgSrc; +} + +/* + * Hides a group of objects by setting to zero the style.opacity CSS property. + */ +function pidoc_hideGroup( elementBaseId, numberOfElements ) +{ + for ( var i = 1; i <= numberOfElements; ++i ) + { + var obj = document.getElementById( elementBaseId + "_" + i.toString() ); + if ( obj ) + obj.style.opacity = 0; + } +} + +/* + * Sets the style.opacity CSS property of an object with id. + */ +function pidoc_setOpacity( id, opacity ) +{ + var obj = document.getElementById( id ); + if ( obj ) + obj.style.opacity = opacity; +} + +/* + * onmousemove event listener - track pointer coordinates relative to window. + * This means that we have to "position: fixed;" all dynamically moving items. + */ +var pidoc_mouseX = 0; +var pidoc_mouseY = 0; +function pidoc_listenMouseMove( event ) +{ + event = event || window.event; // IE-ism + pidoc_mouseX = event.clientX; + pidoc_mouseY = event.clientY; +} + +window.onmousemove = pidoc_listenMouseMove; + +/* + * Document elements that are generated and managed dynamically. + */ +function pidoc_generateDynamicContents() +{ + // Tooltip window + document.writeln( "
" ); +} + +/* + * Open the reference tooltip window for a given reference item at the current + * pointer coordinates. + */ +function pidoc_showReferenceToolTip( refItem ) +{ + var obj = document.getElementById( "referenceToolTip" ); + if ( obj ) + { + obj.innerHTML = refItem.getAttribute( "data-tooltip" ); + obj.style.zIndex = 98; + obj.style.opacity = 1; + + var s = window.getComputedStyle( obj ); + var w = parseInt( s.width ) + parseInt( s.paddingLeft ) + parseInt( s.paddingRight ); + var h = parseInt( s.height ) + parseInt( s.paddingTop ) + parseInt( s.paddingBottom ); + var d = 12; + + if ( pidoc_mouseX + w+d < window.innerWidth ) + obj.style.left = (pidoc_mouseX + d).toString() + "px"; + else + obj.style.left = (pidoc_mouseX - w-d).toString() + "px"; + + if ( pidoc_mouseY - h-d > 0 ) + obj.style.top = (pidoc_mouseY - h-d).toString() + "px"; + else + obj.style.top = (pidoc_mouseY + d).toString() + "px"; + } +} + +/* + * Close the reference tooltip window. + */ +function pidoc_hideReferenceToolTip() +{ + var obj = document.getElementById( "referenceToolTip" ); + if ( obj ) + { + obj.style.zIndex = -1; + obj.style.opacity = 0; + } +} diff --git a/pixinsight_update_bundle/doc/pidoc/scripts/utility.js b/pixinsight_update_bundle/doc/pidoc/scripts/utility.js new file mode 100644 index 0000000..4ad1bc6 --- /dev/null +++ b/pixinsight_update_bundle/doc/pidoc/scripts/utility.js @@ -0,0 +1,91 @@ +/* + * PixInsight Reference Documentation System + * + * Utility JavaScript routines + * Updated 2011 December 2 + * + * Copyright (c) 2010-2011 Pleiades Astrophoto S.L. All Rights Reserved. + */ + +/* + * Add some useful methods to the String object, not part of ECMA. + */ +function String_isEmpty() +{ + return this.length == 0; +} +function String_has( s ) +{ + return this.indexOf( s ) >= 0; +} +function String_beginsWith( s ) +{ + return !this.isEmpty() && this.indexOf( s ) == 0; +} +function String_endsWith( s ) +{ + return !this.isEmpty() && !s.isEmpty() && this.indexOf( s ) == this.length - s.length; +} +String.prototype.isEmpty = String_isEmpty; +String.prototype.has = String_has; +String.prototype.beginsWith = String_beginsWith; +String.prototype.endsWith = String_endsWith; + +/* + * Returns an array with the x,y pixel position of an object on the current document. + */ +function findPos( obj ) +{ + var dx = 0; + var dy = 0; + while ( obj ) + { + dx += obj.offsetLeft; + dy += obj.offsetTop; + obj = obj.offsetParent; + } + return new Array( dx, dy ); +} + +/* + * Toggles the style.display CSS property of an object with id. + */ +function toggleDisplay( id ) +{ + var obj = document.getElementById( id ); + if ( obj ) + obj.style.display = obj.style.display ? "" : "none"; +} + +/* + * Portable routine to set the 'src' attribute of an image selected by its id. + */ +function setImgSrc( imgId, imgSrc ) +{ + var obj = document.getElementById( imgId ); + if ( obj ) + obj.src = imgSrc; +} + +/* + * Hides a group of objects by setting to zero the style.opacity CSS property. + */ +function hideGroup( elementBaseId, numberOfElements ) +{ + for ( var i = 1; i <= numberOfElements; ++i ) + { + var obj = document.getElementById( elementBaseId + "_" + i.toString() ); + if ( obj ) + obj.style.opacity = 0; + } +} + +/* + * Sets the style.opacity CSS property of an object with id. + */ +function setOpacity( id, opacity ) +{ + var obj = document.getElementById( id ); + if ( obj ) + obj.style.opacity = opacity; +} diff --git a/pixinsight_update_bundle/doc/pidoc/titles/pixinsight_javascript_runtime_documentation_title_vert.en.svg b/pixinsight_update_bundle/doc/pidoc/titles/pixinsight_javascript_runtime_documentation_title_vert.en.svg new file mode 100644 index 0000000..4f1625e --- /dev/null +++ b/pixinsight_update_bundle/doc/pidoc/titles/pixinsight_javascript_runtime_documentation_title_vert.en.svg @@ -0,0 +1,3 @@ + + + diff --git a/pixinsight_update_bundle/doc/pidoc/titles/pixinsight_project_documentation_title_vert.en.svg b/pixinsight_update_bundle/doc/pidoc/titles/pixinsight_project_documentation_title_vert.en.svg new file mode 100644 index 0000000..999a27d --- /dev/null +++ b/pixinsight_update_bundle/doc/pidoc/titles/pixinsight_project_documentation_title_vert.en.svg @@ -0,0 +1,2 @@ + + diff --git a/pixinsight_update_bundle/doc/pidoc/titles/pixinsight_reference_documentation_title_vert.en.svg b/pixinsight_update_bundle/doc/pidoc/titles/pixinsight_reference_documentation_title_vert.en.svg new file mode 100644 index 0000000..461f56c --- /dev/null +++ b/pixinsight_update_bundle/doc/pidoc/titles/pixinsight_reference_documentation_title_vert.en.svg @@ -0,0 +1,2 @@ + + diff --git a/pixinsight_update_bundle/doc/scripts/SendMessageToPhone/SendMessageToPhone.html b/pixinsight_update_bundle/doc/scripts/SendMessageToPhone/SendMessageToPhone.html new file mode 100644 index 0000000..a6a0c5b --- /dev/null +++ b/pixinsight_update_bundle/doc/scripts/SendMessageToPhone/SendMessageToPhone.html @@ -0,0 +1,157 @@ + + + + + PixInsight Reference Documentation | SendMessageToPhone + + + + + + + + + + + + + + + +

SendMessageToPhone

+ +
+

By Luca Bartek

+
+ +
+ +
+

A PixInsight script that sends notifications and images directly to your phone via Telegram, iMessage, or Pushover. [more]

+ +
+

Keywords: Telegram, iMessage, Pushover, Notifications, Image, PixInsight, SendMessageToPhone, Setup, Configuration, Messaging

+
+ +

Contents

+

[hide]

+
+ +
+ +
+ +
+

1 Introduction

+

[hide]

+
+

The SendMessageToPhone script allows users to send images to their phones directly via Telegram, iMessage (Mac only), or Pushover. This script also serves as the central configurator of things like the NotificationEventScript.

+

This document will guide you through the process of setting up the SendMessageToPhone script and using it to send notifications.

+
+
+ +
+

2 Setup

+

[hide]

+
+

In order to use the SendMessageToPhone script, you need to configure it to send messages and images via your preferred messaging platform: Telegram, iMessage, or Pushover. Follow the detailed steps below to set it up.

+

Step 1: Open the SendMessageToPhone script in PixInsight's Script Menu.

+

Step 2: Select the messaging platform you would like to use:

+ +
    +
  • Telegram
  • +
  • iMessage
  • +
  • Pushover
  • +
+ +

Step 3: Configure the necessary fields

+ +
    +
  • For Telegram: Enter your Telegram Bot Token. You can obtain the token by creating a bot with BotFather in Telegram Enter the Telegram Chat ID. This can be found by visiting https://api.telegram.org/bot<YourBotToken>/getUpdates, where you replace <YourBotToken> with the token you got from BotFather.
    +
  • +
  • For iMessage: Enter the recipient's AppleID Note: iMessage sending is only supported on macOS. If you're running the script on another platform, please use Telegram or Pushover instead
  • +
  • For Pushover: Enter your User Key and App Token. You can obtain these by creating an account at https://pushover.net and registering an application. Pushover supports image attachments. The script will send both a text message and the active image as a JPEG.
  • +
  • Autostretch: If your target image is still in the linear phase, it's a good idea to do an autostretch on the image, as a non-stretched JPEG will generally show up all-black
  • +
  • Linked: For color images, this determines whether the autostretch should be linked or unlinked. The option is ignored for mono images.
  • +
+ +

Step 4: Run the script

+

The script will be run on the active image window. If there is no active window, no image will be sent.

+
+
+ +
+

3 Usage

+

[hide]

+
+

In addition to running it on the active image window, you may also save (any number of) instances as process icons. They may have different parameters. The process icon can be dragged onto any image and the image will be sent out according to the parameters selected.

+
+
+ +
+

4 Limitations

+

[hide]

+
+

The script has a few limitations:

+ +
    +
  • Telegram: Requires curl to be installed and working. Make sure your system has internet access.
  • +
  • iMessage: Only available on macOS. If you are using Windows or Linux, you need to use Telegram or Pushover.
  • +
  • Pushover: Requires curl. Your image must be converted to a JPEG, and its size must comply with Pushover limits (currently 2.5MB max). If the image is too large, the script will attempt to downsample it.
  • +
+ +
+
+ +
+

5 Troubleshooting

+

[hide]

+
+

If you face any issues with the notifications, here are some common fixes:

+

No notifications received?

+ +
    +
  • Double-check your Telegram Bot Token and Chat ID (for Telegram), Recipient (for iMessage), or User Key / App Token (for Pushover).
  • +
  • Ensure that curl is working properly and is available in your system path.
  • +
+ +

Image not received via Pushover

+ +
    +
  • Ensure the image is not too large. The script automatically reduces the image size if it exceeds recommended limits, but Pushover has a strict 2.5MB file limit.
  • +
  • Make sure the file is being saved as a valid JPEG and your API keys are correct.
  • +
+ +

Error: "No valid target image found"

+ +
    +
  • This error occurs when no active image is found in PixInsight when the script is triggered. Ensure that an image window is open.
  • +
+ +
+
+ +
+ + + + +
+
+ +
+ + + diff --git a/pixinsight_update_bundle/doc/scripts/SendMessageToPhone/images/configuring_telegramBot.png b/pixinsight_update_bundle/doc/scripts/SendMessageToPhone/images/configuring_telegramBot.png new file mode 100644 index 0000000..c93f331 Binary files /dev/null and b/pixinsight_update_bundle/doc/scripts/SendMessageToPhone/images/configuring_telegramBot.png differ diff --git a/pixinsight_update_bundle/src/scripts/TheSpaceKoala/KoalaUtils.js b/pixinsight_update_bundle/src/scripts/TheSpaceKoala/KoalaUtils.js new file mode 100644 index 0000000..1657b8d --- /dev/null +++ b/pixinsight_update_bundle/src/scripts/TheSpaceKoala/KoalaUtils.js @@ -0,0 +1,198 @@ +#include +#include + + + // ================================================================================== + // KoalaUtils.js - Common utilities for The Space Koala scripts + // ================================================================================== + + + function printTheSpaceKoalaBanner() { + console.writeln(' _____ _ '); + console.writeln(' |_ _| |__ ___ '); + console.writeln(' | | | _ \\ / _ \\ '); + console.writeln(' | | | | | | __/ '); + console.writeln(' _|_| |_| |_|\\___| _ __ _ '); + console.writeln(' / ___| _ __ __ _ ___ ___| |/ /___ __ _| | __ _ '); + console.writeln( + ' \\___\\ | _ \\ / _` |/ __/ _ \\ | // _ \\ / _` | |/ _` |' + ); + console.writeln(' ___) | |_) | (_| | (_| __/ . \\ (_) | (_| | | (_| |'); + console.writeln( + ' |____/| .__/ \\__,_|____\\___|_|\\_\\___/ \\__,_|_|\\__,_|' + ); + console.writeln(' |_| '); + } + + function printDebug(msg) { + console.noteln('[DEBUG] ' + msg); + } + + function printInfo(msg) { + console.writeln('[INFO] ' + msg); + } + + function printError(msg) { + console.criticalln('[ERROR] ' + msg); + } + + function raiseError(errorMessage) { + new MessageBox( + errorMessage, + 'ERROR', + StdIcon_Error, + StdButton_Ok + ).execute(); + } + + function raiseInfo(infoMessage) { + new MessageBox( + infoMessage, + 'INFO', + StdIcon_Information, + StdButton_Ok + ).execute(); + } + + function raiseWarning(warningMessage) { + new MessageBox( + warningMessage, + 'WARNING', + StdIcon_Warning, + StdButton_Ok + ).execute(); + } + + /** + * Duplicates an image window and propagates the mainView from the input ImageWindow + * @param {ImageWindow} imageWindow - The source image window to duplicate. + * @param {string} newId - The ID for the duplicated window. + * @returns {ImageWindow} - The new duplicated image window. + */ + function duplicateImageWindow(imageWindow, newId) { + if (!imageWindow || imageWindow.isNull) { + printError('Invalid image window provided for duplication.'); + return null; + } + + var duplicateWindow = new ImageWindow( + 1, + 1, + 1, + imageWindow.mainView.image.bitsPerSample, + imageWindow.mainView.image.sampleType == SampleType_Real, + false, + newId + ); + + with (duplicateWindow.mainView) { + beginProcess(UndoFlag_NoSwapFile); + image.assign(imageWindow.mainView.image); + id = newId; + endProcess(); + } + + printInfo('Image duplicated successfully with ID: ' + newId); + return duplicateWindow; + } + + // Apply an automatic stretch to a given view + function applyAutoStretch(view, linked) { + if (!view || view.isNull) { + printError('Invalid view provided for auto-stretching.'); + return; + } + + printInfo('Applying auto-stretch. Linked: ' + linked); + + let P = new PixelMath(); + if (linked == true) { + P.expression = + '//autostretch\n' + + 'm = (med( $T[0] ) + med( $T[1] ) + med( $T[2] ))/3;\n' + + 'd = (mdev( $T[0] ) + mdev( $T[1] ) + mdev( $T[2] ))/3;\n' + + 'c = min( max( 0, m + C*1.4826*d ), 1 );\n' + + 'mtf( mtf( B, m - c ), max( 0, ($T - c)/~c ) )\n'; + } else { + P.expression = + '//autostretch\n' + + 'c = min( max( 0, med( $T ) + C*1.4826*mdev( $T ) ), 1 );\n' + + 'mtf( mtf( B, med( $T ) - c ), max( 0, ($T - c)/~c ) )'; + } + + P.useSingleExpression = true; + P.symbols = 'C = -2.8,\nB = 0.25,\nc,\nm,\nd'; + P.createNewImage = false; + P.executeOn(view); + + printInfo('Auto-stretch applied successfully.'); + } + + /** + * Downsamples an image view by a given factor. + * @param {View} view - The image view to process. + * @param {number} factor - The zoom factor (e.g., -2 for 2x downsampling). + */ + + function downsampleImage(view, factor) { + if (!view || view.isNull) { + printError('Invalid view provided for downsampling.'); + return; + } + + printInfo('Applying downsampling. Factor: ' + factor); + + var P = new IntegerResample(); + P.zoomFactor = factor; + P.downsamplingMode = IntegerResample.prototype.Average; + P.noGUIMessages = true; + P.executeOn(view); + + printInfo( + 'Resampling complete. New dimensions: ' + + view.image.width + + ' x ' + + view.image.height + ); + } + + /** + * Saves an image window as a JPEG in the specified directory. + * @param {ImageWindow} imageWindow - The image window to save. + * @param {string|null} directory - The target directory (or null for system temp directory). + * @param {string} filename - The desired output filename. + * @returns {string|null} - The full path of the saved JPEG file, or null if failed. + */ + function saveImageAsJpeg(imageWindow, directory, filename) { + if (!imageWindow || imageWindow.isNull) { + printError('Invalid image window provided for JPEG export.'); + return null; + } + + // Use temp directory if directory is null + var outputDirectory = directory ? directory : File.systemTempDirectory; + + // Ensure filename does not end with .jpg or .jpeg (case-insensitive) + var sanitizedFilename = filename.replace(/\.(jpg|jpeg)$/i, '') + '.jpg'; + + // Construct full file path + var jpegPath = outputDirectory + '/' + sanitizedFilename; + + var success = imageWindow.saveAs( + jpegPath, + false, + false, + false, + false, + 'quality=80' + ); + + if (!success) { + printError('Failed to save image as JPEG.'); + return null; + } + + printInfo('JPEG saved successfully at: ' + jpegPath); + return jpegPath; + } +//})(); diff --git a/pixinsight_update_bundle/src/scripts/TheSpaceKoala/NoCodePipelineBuilder.js b/pixinsight_update_bundle/src/scripts/TheSpaceKoala/NoCodePipelineBuilder.js new file mode 100644 index 0000000..97fe1ea --- /dev/null +++ b/pixinsight_update_bundle/src/scripts/TheSpaceKoala/NoCodePipelineBuilder.js @@ -0,0 +1,1061 @@ +//////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////// +// _____ _ // +// |_ _| |__ ___ // +// | | | '_ \ / _ \ // +// | | | | | | __/ // +// |_| |_| |_|\___| _ __ _ // +// / ___| _ __ __ _ ___ ___| |/ /___ __ _| | __ _ // +// \___ \| '_ \ / _` |/ __/ _ \ ' // _ \ / _` | |/ _` | // +// ___) | |_) | (_| | (_| __/ . \ (_) | (_| | | (_| | // +// |____/| .__/ \__,_|\___\___|_|\_\___/ \__,_|_|\__,_| // +// |_| // +//////////////////////////////////////////////////////////////////////////// +// No Code Pipeline Builder +// Version: V1.0.4 - 20250514 +// Author: Luca Bartek, Roberto Sartori +// Website: www.thespacekoala.com +// +// This script should be used via the WeightedBatchPreProcessing script of PixInsight. +// It cannot be run standalone. +// +// This work is licensed under a Creative Commons Attribution-NonCommercial 4.0 International License. +// To view a copy of this license, visit http://creativecommons.org/licenses/by-nc/4.0/ +// +// You are free to: +// 1. Share — copy and redistribute the material in any medium or format +// 2. Adapt — remix, transform, and build upon the material +// +// Under the following terms: +// 1. Attribution — You must give appropriate credit, provide a link to the license, and indicate if changes were made. You may do so in any reasonable manner, but not in any way that suggests the licensor endorses you or your use. +// 2. NonCommercial — You may not use the material for commercial purposes. +// +// @license CC BY-NC 4.0 (http://creativecommons.org/licenses/by-nc/4.0/) +// +// COPYRIGHT © 2025 Luca Bartek, Roberto Sartori. ALL RIGHTS RESERVED. +//////////////////////////////////////////////////////////////////////////// + +/////////////////////////////////// +// PIPELINE BUILDER SCRIPT +/////////////////////////////////// + +/////////////////////////////////////////////////////////////////////////////////// +// MAPPING OF ALL AVAILABLE ENTRY POINTS FOR THE NO CODE PIPELINE BUILDER +/////////////////////////////////////////////////////////////////////////////////// + +// Define available master file types +var WBPPMasterTypes = ['MASTER_LIGHT', 'DRIZZLE', 'RECOMBINED']; + +// Define available master file variants +var WBPPMasterVariants = ['REGULAR', 'CROPPED']; + +var customStep = { + onCalibrationStart: 1, + onCalibrationEnd: 2, + onLPSStart: 3, + onLSPEnd: 4, + onCCStart: 5, + onCCEnd: 6, + onDebayerStart: 7, + onDebayerEnd: 8, + onPreProcessEnd: 9, + onPostProcessStart: 10, + onRegistrationStart: 11, + onRegistrationEnd: 12, + onLNStart: 13, + onLNEnd: 14, + onIntegrationStart: 15, + onIntegrationEnd: 16, + onPostProcessEnd: 17, +}; + +var WBPPProcessedMasterGroups = {}; + +/////////////////////////////////////////////////////////////////////////////////// +// RETURNS THE NAME OF A CUSTOM STEP - USED FOR NAMING IN THE PIPELINE +/////////////////////////////////////////////////////////////////////////////////// +StackEngine.prototype.customStepName = function (customStepId) { + for (var key in customStep) { + if (customStep[key] === customStepId) { + return key; // Return the key name if the value matches + } + } + return null; // Return null if not found +}; + +/////////////////////////////////////////////////////////////////////////////////// +// CALCULATES THE FACTOR TO CALCULATE THE SPACE REQUIRED FOR A PROCESS +/////////////////////////////////////////////////////////////////////////////////// +StackEngine.prototype.processSizeFactor = function (pixiProcess) { + if (pixiProcess.processId() == 'IntegerResample') { + return 1 / pixiProcess.zoomFactor / pixiProcess.zoomFactor; + } else { + return 1; + } +}; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// EXTRACTS PROCESSES FROM THE WBPP CONTAINER, APPLIES INHERITANCE RULES +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +StackEngine.prototype.extractProcessesFromWBPP = function (currentStep) { + let wbppContainer = ProcessInstance.fromIcon('WBPP'); + if (!wbppContainer) { + console.warning( + 'Error: WBPP process container not found. Aborting pipeline setup.' + ); + return []; + } + + function shallowCopy(obj) { + var copy = {}; + for (var key in obj) { + if (obj.hasOwnProperty(key)) { + copy[key] = obj[key]; // Copy each key-value pair + } + } + return copy; + } + + function extractProcesses( + pixiProcess, + inheritedAttributes, + processList, + currentStep + ) { + // Inherit attributes from parent + var attributes = shallowCopy(inheritedAttributes); + + //extract values and concatenate to inherited ones (if any) + if (pixiProcess.description) { + // Split description using spaces, underscores, commas, or semicolons + let parts = pixiProcess.description().split(/[\s_,;]+/); + + for (var i = 0; i < parts.length; i++) { + var pair = parts[i].split(/[=-]+/); // Split by "=", "-" + + if (pair.length === 2) { + var key = pair[0].toLowerCase(); + var value = pair[1]; + + attributes[key] = value; // Overwrite if key already exists + } + } + } + + //then if it's a process container call the function again on each child + if (pixiProcess.processId() == 'ProcessContainer') { + for (let i = 0; i < pixiProcess.length; i++) { + let child = pixiProcess.at(i); + extractProcesses(child, attributes, processList, currentStep); + } + } + //otherwise add current process to the list of processes returned by the function + else { + // Ensure the process has a step and it is the one currently evaluated + if (!attributes.step || customStep[attributes.step] !== currentStep) { + return; //don't add it if step is missing + } + processList.push({ + process: pixiProcess, // Store process instance + attributes: attributes, + }); + } + } + + var processes = []; + extractProcesses(wbppContainer, {}, processes, currentStep); + + return processes; +}; + +/////////////////////////////////////////////////////////////////////////////////// +// CUSTOM OPERATION - DYNAMICALLY ADDS A PROCESSING STEP TO A GROUP AND EXECUTES IT +/////////////////////////////////////////////////////////////////////////////////// +StackEngine.prototype.CustomOperation = function ( + frameGroup, + pixiProcess, + currentStep, + customStepSeqNr, + spaceRequiredFactor, + isMasterStep, + frameGroupIndex +) { + // Construct the process name based on the operation type + var processName = ''; + if (pixiProcess.processId() == 'NoOperation') { + processName = 'Custom ' + engine.customStepName(currentStep) + ' step(s)'; + } else { + processName = + ' ' + + engine.customStepName(currentStep) + + ' #' + + customStepSeqNr + + ': ' + + pixiProcess.processId(); + } + + // Inherit from BPPOperationBlock to track execution + this.__base__ = BPPOperationBlock; + this.__base__(processName, frameGroup, true /* trackable */); + + /* + * Compute the required disk space for execution. + * We estimate based on frame count and frame size. + */ + this.spaceRequired = () => { + if (!isMasterStep) { + return ( + frameGroup.fileItems.length * + frameGroup.frameSize() * + spaceRequiredFactor + ); + } else { + return 1 * frameGroup.frameSize() * spaceRequiredFactor; // TODO: Replace 1 with actual master frame count + } + }; + + /** + * Defines standard group data for logging and debugging. + **/ + this.envForScript = () => ({ + name: processName, + status: this.status, + statusMessage: this.statusMessage, + group: frameGroup, + }); + + this._run = function () { + // If this is a master step, clear existing active frames and retrieve master files + if (isMasterStep) { + + while (frameGroup.fileItems.length > 0) { + frameGroup.removeItem(0); + } + + let masterFiles = []; + // Loop through all master type and variant combinations + for (let type of WBPPMasterTypes) { + for (let variant of WBPPMasterVariants) { + let masterKey = type + '_' + variant; + + // Retrieve the processed master file first, otherwise fallback to the original master file + let masterFile; + if (WBPPProcessedMasterGroups.hasOwnProperty(frameGroupIndex)) { + masterFile = WBPPProcessedMasterGroups[frameGroupIndex][masterKey]; + } else { + masterFile = frameGroup.getMasterFileName( + WBPPMasterType[type], + WBPPMasterVariant[variant] + ); + } + + if (masterFile) { + masterFiles.push(masterFile); + } + } + } + + // If no master files are found, terminate the function + if (masterFiles.length === 0) { + console.warning('[WARNING] No master files found! Exiting function.'); + return OperationBlockStatus.FAILED; + } + + // Convert master files into `FileItem` instances and add them to `frameGroup.fileItems` + for (let masterFile of masterFiles) { + let fileItem = new FileItem(masterFile); + frameGroup.fileItems.push(fileItem); + } + } + + // Retrieve the active frames after modifications + let activeFrames = frameGroup.activeFrames(); + + // If no active frames exist, terminate the function + if (activeFrames.length == 0) return OperationBlockStatus.DONE; + + // Set the output directory, removing frameGroup.folderName() for master files + let outputFolder = WBPPUtils.existingDirectory( + engine.outputDirectory + + '/' + + engine.customStepName(currentStep) + + (isMasterStep ? '' : '/' + frameGroup.folderName()) + ); + + // Define the custom process instance + var P = pixiProcess; + + // Track the number of successfully processed frames + let nSuccess = 0; + let nFailed = 0; + + // Loop through all active frames and process them + for (let i = 0; i < activeFrames.length; i++) { + let filePath = activeFrames[i].current; + + // Open the image file + let w = ImageWindow.open(filePath); + if (w.length == 0) { + // Mark frame as failed if unable to load + activeFrames[i].processingFailed(); + nFailed++; + continue; + } + + // Close all additional windows except the first + for (let k = 1; k < w.length; k++) w[k].forceClose(); + + // Execute the custom process + let view = w[0].mainView; + let outputFile = + outputFolder + '/' + File.extractNameAndExtension(filePath); + + if (P.executeOn(view)) { + // Save the processed image + w[0].saveAs(outputFile, false, false, false, false); + } + + // If output file exists, mark processing as successful + if (File.exists(outputFile)) { + activeFrames[i].processingSucceeded( + Number(currentStep * 1000000) + Number(customStepSeqNr * 1000), + outputFile + ); + nSuccess++; + // Identify the correct master type and store the processed file + for (let type of WBPPMasterTypes) { + for (let variant of WBPPMasterVariants) { + let masterKey = type + '_' + variant; + let existingMasterFile = frameGroup.getMasterFileName( + WBPPMasterType[type], + WBPPMasterVariant[variant] + ); + + if ( + existingMasterFile && + File.extractName(existingMasterFile) === + File.extractName(outputFile) + ) { + if (!WBPPProcessedMasterGroups.hasOwnProperty(frameGroupIndex)) { + WBPPProcessedMasterGroups[frameGroupIndex] = {}; // Create a new dictionary for key j + } + WBPPProcessedMasterGroups[frameGroupIndex][masterKey] = + outputFile; + break; // Exit inner loop once we find a match + } + } + } + } else { + // If writing failed, mark frame as failed + activeFrames[i].processingFailed(); + nFailed++; + } + w[0].forceClose(); + } + + // Save the step status information + this.statusMessage = WBPPUtils.resultCountToString(0, nSuccess, nFailed, 'processed'); + this.hasWarnings = nFailed > 0; + return OperationBlockStatus.DONE; + }; +}; + +// Assign the prototype to BPPOperationBlock +StackEngine.prototype.CustomOperation.prototype = new BPPOperationBlock(); + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// FILTER PREVIOUSLY RETRIEVED PROCESS LIST FOR EACH OF THE FRAME GROUPS AND CALL THE SCHEDULER TO ADD THE STEP TO THE PIPELINE AND EXECUTE THE PROCESS +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +StackEngine.prototype.getAndScheduleProcesses = function ( + groups, + currentStep, + isMasterStep +) { + var imageTypeMapping = { + light: ImageType.LIGHT, + dark: ImageType.DARK, + bias: ImageType.BIAS, + flat: ImageType.FLAT, + }; + + var processesForCurrentStep = engine.extractProcessesFromWBPP(currentStep); + // loop on full list of processes applicable to current step + // filter them and apply only on applicable groups + // schedule those processes for the groups + for (let j = 0; j < groups.length; ++j) { + var processListToApply = []; + for (let i = 0; i < processesForCurrentStep.length; ++i) { + var processToAdd = true; + if ( + groups[j].associatedRGBchannel != WBPPAssociatedChannel.COMBINED_RGB && + groups[j].imageType == ImageType.LIGHT + ) { + for (var key in processesForCurrentStep[i].attributes) { + if (key === 'step') { + continue; + } + //eventually if we re-add support for filtering on image type + /* else if (key === 'imagetype') { + // check for type of group if light/bias/dark/flat + if ( + groups[j].imageType != + imageTypeMapping[ + processesForCurrentStep[i].attributes[key].toLowerCase() + ] + ) { + processToAdd = false; + break; + } + } + */ + else { + // for any other keyword we assume those are grouping keywords and we look for matches + for (var groupkeyword in groups[j].keywords) { + if (groupkeyword != key) { + continue; + } else if ( + groups[j].keywords[groupkeyword] != + processesForCurrentStep[i].attributes[key] + ) { + processToAdd = false; + break; + } + } + } + } + + // we need to add this process to the list of custom operations + if (processToAdd) { + processListToApply.push(processesForCurrentStep[i].process); + } + } + } + if (processListToApply.length > 0) { + var noOpProc = new NoOperation(); + processListToApply.unshift(noOpProc); + } + + var spaceRequiredFactor = 1; + // loop through all processes for the group to get the overall size multiplying factor + for (let p = 0; p < processListToApply.length; ++p) { + spaceRequiredFactor *= engine.processSizeFactor(processListToApply[p]); + } + for (let p = 0; p < processListToApply.length; ++p) { + //this is the first custom operation, we initialize the size counter for it + if (!engine.WS.customOperation) { + engine.WS.customOperation = { + label: 'Custom Operations', + size: 0, + }; + } + //we pass the "space required factor" - to be enhanced later in case we want to add + //specific factors for specific processes for a more accurate calculation + if (p > 0) { + spaceRequiredFactor = 0; + } + // instantiate the operation, update the operation size counter and schedule the operation + let customOperation = new engine.CustomOperation( + groups[j], + processListToApply[p], + currentStep, + p, + spaceRequiredFactor, + isMasterStep, + j + ); + engine.WS.customOperation.size += customOperation.spaceRequired(); + engine.operationQueue.addOperation(customOperation); + } + } +}; + +////////////////////////////////// +// PIPELINE BUILDER INSTRUCTIONS +/////////////////////////////////// + +let build = () => { + + // pre-process groups first + let groupsPRE = engine.groupsManager.groupsForMode(WBPPGroupingMode.PRE); + + // CALIBRATION + for (let i = 0; i < groupsPRE.length; ++i) { + if (groupsPRE[i].imageType == ImageType.LIGHT) { + let logEnv = { + processLogger: engine.processLogger, + group: groupsPRE[i], + }; + + // log header + engine.operationQueue.addOperationBlock(env => { + env.params.processLogger.newLine(); + env.params.processLogger.addMessage( + env.params.group.logStringHeader('LIGHT FRAMES CALIBRATION') + ); + }, logEnv); + + // calibration step (only if calibration masters are found) + let cg = engine.getCalibrationGroupsFor(groupsPRE[i]); + if (cg.masterBias || cg.masterDark || cg.masterFlat) { + let calibrationOperation = new engine.calibrationOperation( + groupsPRE[i] + ); + engine.WS.calibrationLight.size += calibrationOperation.spaceRequired(); + engine.operationQueue.addOperation(calibrationOperation); + } + + // log footer + engine.operationQueue.addOperationBlock(env => { + env.params.processLogger.addMessage(env.params.group.logStringFooter()); + env.params.processLogger.newLine(); + }, logEnv); + } + } + + var groupsToCalibrate = groupsPRE.slice(); + for (var j = groupsToCalibrate.length - 1; j >= 0; j--) { + var cg = engine.getCalibrationGroupsFor(groupsToCalibrate[j]); + + // Check if none of the conditions are true + if (!(cg.masterBias || cg.masterDark || cg.masterFlat)) { + // Remove the group from the list if the condition is met + groupsToCalibrate.splice(j, 1); + } + } + if (groupsToCalibrate.length > 0) { + engine.getAndScheduleProcesses( + groupsToCalibrate, + customStep.onCalibrationEnd, + false + ); + } + for (let i = 0; i < groupsPRE.length; ++i) { + // LINEAR PATTERN SUBTRACTION + if (engine.linearPatternSubtraction) { + // engine.getAndScheduleProcesses(groupsPRE, customStep.onLPSStart, false); + + let logEnv = { + processLogger: engine.processLogger, + }; + // log header + engine.operationQueue.addOperationBlock(env => { + env.params.processLogger.newLine(); + env.params.processLogger.addMessage( + '******************** LINEAR DEFECTS CORRECTION ********************' + ); + }, logEnv); + + let LPSOperation = new engine.LPSOperation(); + engine.WS.LPS.size += LPSOperation.spaceRequired(); + engine.operationQueue.addOperation(LPSOperation); + + // log footer + engine.operationQueue.addOperationBlock(env => { + env.params.processLogger.addMessage('' + SEPARATOR + ''); + env.params.processLogger.newLine(); + }, logEnv); + // engine.getAndScheduleProcesses(groupsPRE, customStep.onLPSEnd, false); + } + } + // COSMETIC CORRECTON AND DEBAYER + + for (let i = 0; i < groupsPRE.length; ++i) { + if (groupsPRE[i].imageType == ImageType.LIGHT) { + let logEnv = { + processLogger: engine.processLogger, + group: groupsPRE[i], + }; + + let needsCC = + groupsPRE[i].ccData.CCTemplate && + groupsPRE[i].ccData.CCTemplate.length > 0; + let needsDebayer = groupsPRE[i].isCFA; + + if (!needsCC && !needsDebayer) continue; + + // log header + engine.operationQueue.addOperationBlock(env => { + env.params.processLogger.newLine(); + env.params.processLogger.addMessage( + env.params.group.logStringHeader( + (needsCC ? 'COSMETIZATION' : '') + + (needsCC && needsDebayer ? ' and ' : '') + + (needsDebayer ? 'DEBAYERING' : '') + ) + ); + }, logEnv); + + // cosmetic correction + if (needsCC) { + let cosmeticCorretcionOperation = + new engine.CosmeticCorrectionOperation(groupsPRE[i]); + engine.WS.cosmeticCorrection.size += + cosmeticCorretcionOperation.spaceRequired(); + engine.operationQueue.addOperation(cosmeticCorretcionOperation); + } + + // debayer + if (needsDebayer) { + let debayerOperation = new engine.DebayerOperation(groupsPRE[i]); + engine.WS.debayer.size += debayerOperation.spaceRequired(); + engine.operationQueue.addOperation(debayerOperation); + } + + // log footer + engine.operationQueue.addOperationBlock(env => { + env.params.processLogger.addMessage(env.params.group.logStringFooter()); + env.params.processLogger.newLine(); + }, logEnv); + } + } + // POST-PROCESS GROUPS + let groupsPOST = engine.groupsManager + .groupsForMode(WBPPGroupingMode.POST) + .filter(g => g.isActive); + + // REFERENCE FRAME DATA PREPARATION + if (!engine.reuseLastReferenceFrames) + engine.operationQueue.addOperation( + new engine.ReferenceFrameDataPreparationOperation() + ); + + // MEASUREMENT OPERATION + let generateSubframesWeights = + engine.subframeWeightingEnabled && + (engine.subframesWeightsMethod == WBPPSubframeWeightsMethod.FORMULA || + engine.integrate); + let measureForBestFrameSelection = + (engine.imageRegistration || engine.fastMode) && + engine.bestFrameReferenceMethod != WBPPBestReferenceMethod.MANUAL && + !engine.reuseLastReferenceFrames; + let measureImages = + generateSubframesWeights || + measureForBestFrameSelection || + engine.localNormalization; + if (!engine.groupsManager.isEmpty() && measureImages) { + engine.operationQueue.addOperation(new engine.MeasurementOperation()); + if ( + engine.subframeWeightingEnabled && + engine.subframesWeightsMethod == WBPPSubframeWeightsMethod.FORMULA + ) + engine.operationQueue.addOperation( + new engine.CustomFormulaWeightsGenerationOperation() + ); + if ( + engine.subframesWeightsMethod != WBPPSubframeWeightsMethod.PSFScaleSNR && + engine.integrate && + engine.groupsManager.regularIntegrationGroupExists() + ) + engine.operationQueue.addOperation( + new engine.BadFramesRejectionOperation() + ); + } + + // SET THE REFERENCE FRAME (for both post calibration group with or without fast integration ) + let regularRegistrationGroupExists = + engine.imageRegistration && + engine.groupsManager.regularIntegrationGroupExists(); + if ( + !engine.groupsManager.isEmpty() && + (regularRegistrationGroupExists || + (engine.groupsManager.fastIntegrationGroupExists() && engine.integrate)) + ) + if (!engine.reuseLastReferenceFrames) + engine.operationQueue.addOperation( + new engine.ReferenceFrameSelectionOperation() + ); + + if (groupsPRE.length > 0) { + engine.getAndScheduleProcesses( + groupsPRE, + customStep.onPreProcessEnd, + false + ); + } + // exit if no registration, local normalization and integration needs to be performed + if ( + !generateSubframesWeights && + !engine.imageRegistration && + !engine.localNormalization && + !engine.integrate + ) + return; + + if (groupsPOST.length > 0) { + engine.getAndScheduleProcesses( + groupsPOST, + customStep.onPostProcessStart, + false + ); + } + + // POST-PROCESS GROUPS - REGISTRATION + if (engine.imageRegistration) { + engine.getAndScheduleProcesses( + groupsPOST, + customStep.onRegistrationStart, + false + ); + for (let i = 0; i < groupsPOST.length; ++i) { + let standardPostCalibrationProcessing = + groupsPOST[i].associatedRGBchannel != + WBPPAssociatedChannel.COMBINED_RGB && + !groupsPOST[i].fastIntegrationData.enabled; + + if (standardPostCalibrationProcessing) { + let logEnv = { + processLogger: engine.processLogger, + group: groupsPOST[i], + }; + + // log header + engine.operationQueue.addOperationBlock(env => { + env.params.processLogger.addMessage( + env.params.group.logStringHeader('IMAGE REGISTRATION') + ); + }, logEnv); + + let registrationOperation = new engine.RegistrationOperation( + groupsPOST[i] + ); + engine.WS.registration.size += registrationOperation.spaceRequired(); + engine.operationQueue.addOperation(registrationOperation); + + // log footer + engine.operationQueue.addOperationBlock(env => { + env.params.processLogger.addMessage( + env.params.group.logStringFooter() + ); + env.params.processLogger.newLine(); + }, logEnv); + } + } + engine.getAndScheduleProcesses( + groupsPOST, + customStep.onRegistrationEnd, + false + ); + } + + // POST-PROCESS GROUPS - LOCAL NORMALIZATION REFERENCE FRAME SELECTION + if (engine.localNormalization) + for (let i = 0; i < groupsPOST.length; ++i) { + let standardPostCalibrationProcessing = + groupsPOST[i].associatedRGBchannel != + WBPPAssociatedChannel.COMBINED_RGB && + !groupsPOST[i].fastIntegrationData.enabled; + + if (standardPostCalibrationProcessing) { + let logEnv = { + processLogger: engine.processLogger, + group: groupsPOST[i], + }; + + // log header + engine.operationQueue.addOperationBlock(env => { + env.params.processLogger.addMessage( + env.params.group.logStringHeader( + 'LOCAL NORMALIZATION - REFERENCE FRAME SELECTION' + ) + ); + }, logEnv); + + if (!engine.reuseLastLNReferenceFrames) { + let lnReferenceFrameSelectionOperation = + new engine.LocalNormalizationReferenceFrameSelectionOperation( + groupsPOST[i] + ); + engine.WS.localNormalization.size += + lnReferenceFrameSelectionOperation.spaceRequired(); + engine.operationQueue.addOperation( + lnReferenceFrameSelectionOperation + ); + } + + // log footer + engine.operationQueue.addOperationBlock(env => { + env.params.processLogger.addMessage( + env.params.group.logStringFooter() + ); + env.params.processLogger.newLine(); + }, logEnv); + } + } + + // POST-PROCESS GROUPS - LOCAL NORMALIZATION + if (engine.localNormalization) + for (let i = 0; i < groupsPOST.length; ++i) { + let standardPostCalibrationProcessing = + groupsPOST[i].associatedRGBchannel != + WBPPAssociatedChannel.COMBINED_RGB && + !groupsPOST[i].fastIntegrationData.enabled; + + if (standardPostCalibrationProcessing) { + let logEnv = { + processLogger: engine.processLogger, + group: groupsPOST[i], + }; + + // log header + engine.operationQueue.addOperationBlock(env => { + env.params.processLogger.addMessage( + env.params.group.logStringHeader('LOCAL NORMALIZATION') + ); + }, logEnv); + + let lnOperation = new engine.LocalNormalizationOperation(groupsPOST[i]); + engine.WS.localNormalization.size += lnOperation.spaceRequired(); + engine.operationQueue.addOperation(lnOperation); + + // log footer + engine.operationQueue.addOperationBlock(env => { + env.params.processLogger.addMessage( + env.params.group.logStringFooter() + ); + env.params.processLogger.newLine(); + }, logEnv); + } + } + + // POST-PROCESS GROUPS - IMAGE INTGEGRATION + + if (engine.integrate) { + // REGULAR INTEGRATION + engine.getAndScheduleProcesses( + groupsPOST, + customStep.onIntegrationStart, + false + ); + + for (let i = 0; i < groupsPOST.length; ++i) { + let standardPostCalibrationProcessing = + groupsPOST[i].associatedRGBchannel != + WBPPAssociatedChannel.COMBINED_RGB; + + if ( + standardPostCalibrationProcessing && + !groupsPOST[i].fastIntegrationData.enabled + ) { + // Image Integration + let logEnv = { + processLogger: engine.processLogger, + group: groupsPOST[i], + }; + + // log header + engine.operationQueue.addOperationBlock(env => { + env.params.processLogger.addMessage( + env.params.group.logStringHeader('IMAGE INTEGRATION') + ); + }, logEnv); + + let imageIntegrationOperation = new engine.ImageIntegrationOperation( + groupsPOST[i] + ); + engine.WS.integration.size += imageIntegrationOperation.spaceRequired(); + engine.operationQueue.addOperation(imageIntegrationOperation); + + // log footer + engine.operationQueue.addOperationBlock(env => { + env.params.processLogger.addMessage( + env.params.group.logStringFooter() + ); + env.params.processLogger.newLine(); + }, logEnv); + } + } + + // FAST INTEGRATION + + for (let i = 0; i < groupsPOST.length; ++i) { + let standardPostCalibrationProcessing = + groupsPOST[i].associatedRGBchannel != + WBPPAssociatedChannel.COMBINED_RGB; + + if ( + standardPostCalibrationProcessing && + groupsPOST[i].fastIntegrationData.enabled + ) { + // Fast Integration + let logEnv = { + processLogger: engine.processLogger, + group: groupsPOST[i], + }; + + // log header + engine.operationQueue.addOperationBlock(env => { + env.params.processLogger.addMessage( + env.params.group.logStringHeader('FAST INTEGRATION') + ); + }, logEnv); + + let fastIntegrationOperation = new engine.FastIntegrationOperation( + groupsPOST[i] + ); + engine.WS.fastIntegration.size += + fastIntegrationOperation.spaceRequired(); + let imageIntegrationOperation = new engine.ImageIntegrationOperation( + groupsPOST[i] + ); + engine.WS.integration.size += imageIntegrationOperation.spaceRequired(); + engine.operationQueue.addOperation(fastIntegrationOperation); + + // log footer + engine.operationQueue.addOperationBlock(env => { + env.params.processLogger.addMessage( + env.params.group.logStringFooter() + ); + env.params.processLogger.newLine(); + }, logEnv); + } + } + + // DRIZZLE INTEGRATION + + for (let i = 0; i < groupsPOST.length; ++i) { + let standardPostCalibrationProcessing = + groupsPOST[i].associatedRGBchannel != + WBPPAssociatedChannel.COMBINED_RGB; + + if ( + standardPostCalibrationProcessing && + groupsPOST[i].isDrizzleEnabled() + ) { + // Fast Integration + let logEnv = { + processLogger: engine.processLogger, + group: groupsPOST[i], + }; + + // log header + engine.operationQueue.addOperationBlock(env => { + env.params.processLogger.addMessage( + env.params.group.logStringHeader('DRIZZLE INTEGRATION') + ); + }, logEnv); + + let drizzleIntegrationOperation = + new engine.DrizzleIntegrationOperation(groupsPOST[i]); + engine.WS.integration.size += + drizzleIntegrationOperation.spaceRequired(); + engine.operationQueue.addOperation(drizzleIntegrationOperation); + + // log footer + engine.operationQueue.addOperationBlock(env => { + env.params.processLogger.addMessage( + env.params.group.logStringFooter() + ); + env.params.processLogger.newLine(); + }, logEnv); + } + } + + // AUTO CROP + + if (engine.integrate && engine.autocrop && groupsPOST.length > 0) { + let logEnv = { + processLogger: engine.processLogger, + }; + + // log header + engine.operationQueue.addOperationBlock(env => { + console.writeln(); + env.params.processLogger.addMessage( + '*********************** AUTO CROP ***********************' + ); + console.writeln(SEPARATOR); + console.writeln('* Begin autocrop of master light frames'); + console.writeln(SEPARATOR); + }, logEnv); + + let autocropOperation = new engine.AutoCropOperation(); + engine.WS.integration.size += autocropOperation.spaceRequired(); + engine.operationQueue.addOperation(autocropOperation); + + // log footer + engine.operationQueue.addOperationBlock(env => { + env.params.processLogger.addMessage('' + SEPARATOR + ''); + env.params.processLogger.newLine(); + console.writeln(SEPARATOR); + console.writeln('* End autocrop of master light frames'); + console.writeln(SEPARATOR); + }, logEnv); + } + + // process the RGB recombination groups, this needs to be done at the end to ensure that all + // gray R,G and B masters have been integrated + for (let i = 0; i < groupsPOST.length; ++i) { + let logEnv = { + processLogger: engine.processLogger, + group: groupsPOST[i], + }; + + // RGB RECOMBINATION + if ( + groupsPOST[i].associatedRGBchannel == WBPPAssociatedChannel.COMBINED_RGB + ) { + // log header + engine.operationQueue.addOperationBlock(env => { + env.params.processLogger.addMessage( + env.params.group.logStringHeader('RGB COMBINATION') + ); + }, logEnv); + + // RGB Recombination + let recombinationOperation = new engine.RGBRecombinationOperation( + groupsPOST[i] + ); + engine.WS.recombination.size += recombinationOperation.spaceRequired(); + engine.operationQueue.addOperation(recombinationOperation); + + // log footer + engine.operationQueue.addOperationBlock(env => { + env.params.processLogger.addMessage( + env.params.group.logStringFooter() + ); + env.params.processLogger.newLine(); + }, logEnv); + } + } + + // Plate solve + if (engine.integrate && engine.platesolve) { + for (let i = 0; i < groupsPOST.length; ++i) { + // Image Integration + let logEnv = { + processLogger: engine.processLogger, + group: groupsPOST[i], + }; + + // log header + engine.operationQueue.addOperationBlock(env => { + env.params.processLogger.addMessage( + env.params.group.logStringHeader('ASTROMETRIC SOLUTION') + ); + }, logEnv); + + let plateSolveOperation = new engine.PlateSolveOperation(groupsPOST[i]); + engine.operationQueue.addOperation(plateSolveOperation); + + engine.operationQueue.addOperationBlock(env => { + env.params.processLogger.addMessage( + env.params.group.logStringFooter('COMPLETED') + ); + }, logEnv); + } + } + + //Very last step on the final integrated file(s) + if (engine.integrate) { + engine.getAndScheduleProcesses( + groupsPOST, + customStep.onPostProcessEnd, + true + ); + } + } +}; + +build(); diff --git a/pixinsight_update_bundle/src/scripts/TheSpaceKoala/NotificationEventScript.js b/pixinsight_update_bundle/src/scripts/TheSpaceKoala/NotificationEventScript.js new file mode 100644 index 0000000..8327ece --- /dev/null +++ b/pixinsight_update_bundle/src/scripts/TheSpaceKoala/NotificationEventScript.js @@ -0,0 +1,542 @@ +//////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////// +// _____ _ // +// |_ _| |__ ___ // +// | | | '_ \ / _ \ // +// | | | | | | __/ // +// |_| |_| |_|\___| _ __ _ // +// / ___| _ __ __ _ ___ ___| |/ /___ __ _| | __ _ // +// \___ \| '_ \ / _` |/ __/ _ \ ' // _ \ / _` | |/ _` | // +// ___) | |_) | (_| | (_| __/ . \ (_) | (_| | | (_| | // +// |____/| .__/ \__,_|\___\___|_|\_\___/ \__,_|_|\__,_| // +// |_| // +//////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////// +// +// Notification Event Script +// Version: V1.0.2 //20250521 +// Author: Luca Bartek +// Website: www.thespacekoala.com +// +// Original text-based Telegram notification script idea & implementation by Marco Manenti as per +// https://github.com/verbavolant/eventSendMessageToTelegram +// +// Pushover idea and implementation by Brian Weber, see +// https://blog.briangweber.com/pushover-setup/ +// +// This script should be used via the WeightedBatchPreProcessing script of PixInsight. +// It cannot be run standalone. +// +// This work is licensed under a Creative Commons Attribution-NonCommercial 4.0 International License. +// To view a copy of this license, visit http://creativecommons.org/licenses/by-nc/4.0/ +// +// You are free to: +// 1. Share — copy and redistribute the material in any medium or format +// 2. Adapt — remix, transform, and build upon the material +// +// Under the following terms: +// 1. Attribution — You must give appropriate credit, provide a link to the license, and indicate if changes were made. +// You may do so in any reasonable manner, but not in any way that suggests the licensor endorses you or your use. +// 2. NonCommercial — You may not use the material for commercial purposes. +// +// @license CC BY-NC 4.0 (http://creativecommons.org/licenses/by-nc/4.0/) +// +// COPYRIGHT © 2025 Luca Bartek. ALL RIGHTS RESERVED. +//////////////////////////////////////////////////////////////////////////// + +//////////////////////////////////////////////////////////////////////////// +//@todo temporary redecleration of a bunch of stuff - to be removed once I know how to import them +//////////////////////////////////////////////////////////////////////////// +var PLATFORMS = { + TELEGRAM: 1, + IMESSAGE: 2, + PUSHOVER: 3, +}; + +if (typeof SendMessageToPhoneConstants === 'undefined') + SendMessageToPhoneConstants = {}; + +SendMessageToPhoneConstants.PHONE_PLATFORM_KEY = 'SendMessageToPhone/platform'; +SendMessageToPhoneConstants.TELEGRAM_TOKEN_KEY = 'SendMessageToPhone/telegramBotToken'; +SendMessageToPhoneConstants.TELEGRAM_CHAT_ID_KEY = 'SendMessageToPhone/telegramChatId'; +SendMessageToPhoneConstants.MESSAGE_KEY = 'SendMessageToPhone/message'; +SendMessageToPhoneConstants.IMESSAGE_RECIPIENT_KEY = 'SendMessageToPhone/imessageRecipient'; +SendMessageToPhoneConstants.PUSHOVER_USER_KEY = 'SendMessageToPhone/pushoverUser'; +SendMessageToPhoneConstants.PUSHOVER_TOKEN_KEY = 'SendMessageToPhone/pushoverToken'; +SendMessageToPhoneConstants.DO_STRETCH_KEY = 'SendMessageToPhone/doStretch'; +SendMessageToPhoneConstants.LINKED_KEY = 'SendMessageToPhone/linked'; + + +var savedConfig = { + platform: + Settings.read( + SendMessageToPhoneConstants.PHONE_PLATFORM_KEY, + 5 //DataType_Int32 + ) || PLATFORMS.TELEGRAM, + telegramBotToken: + Settings.read( + SendMessageToPhoneConstants.TELEGRAM_TOKEN_KEY, + 14 //DataType_UCString + ) || '', + telegramChatId: (function () { + var v = Settings.read( + SendMessageToPhoneConstants.TELEGRAM_CHAT_ID_KEY, + 14 //DataType_UCString + ); + return typeof v === 'undefined' ? 0 : v; + })(), + message: + Settings.read( + SendMessageToPhoneConstants.MESSAGE_KEY, + 14 //DataType_UCString + ) || ':)', + imessageRecipient: + Settings.read( + SendMessageToPhoneConstants.IMESSAGE_RECIPIENT_KEY, + 14 //DataType_UCString + ) || '', + pushoverUser: + Settings.read( + SendMessageToPhoneConstants.PUSHOVER_USER_KEY, + 14 //DataType_UCString + ) || '', + pushoverToken: + Settings.read( + SendMessageToPhoneConstants.PUSHOVER_TOKEN_KEY, + 14 //DataType_UCString + ) || '', + }; + +let sendMessage = function ( + platform, + telegramToken, + telegramChatId, + imessageRecipient, + pushoverUser, + pushoverToken, + message +) { + + if (platform === PLATFORMS.TELEGRAM) { + sendMessageToTelegram( + telegramToken, + telegramChatId, + message + ); + } else if (platform === PLATFORMS.IMESSAGE) { + sendMessageToIMessage( + imessageRecipient, + message + ); + } else if (platform === PLATFORMS.PUSHOVER) { + sendMessageToPushover( + pushoverUser, + pushoverToken, + message + ); + } else { + printError('Please configure platform in the SendMessageToPhone script'); + } +} + + +let sendMessageToTelegram = function (botToken, chatId, message) { + var url = "https://api.telegram.org/bot" + botToken + "/sendMessage"; + var args = [ + "-s", "-X", "POST", + url, + "-d", "chat_id=" + chatId, + "-d", "text=" + message + ]; + + var p = new ExternalProcess(); + ExternalProcess.execute('curl', args); +} + + +let sendImageToTelegram = function (imagePath, telegramToken, chatId, message) { + const url = 'https://api.telegram.org/bot' + telegramToken + '/sendPhoto'; + let args = [ + '-s', + '-X', + 'POST', + url, + '-F', + 'chat_id=' + chatId, + '-F', + 'photo=@' + imagePath, + ]; + + if (message) { + args[args.length] = '-F'; + args[args.length] = 'caption=' + message; + } + + // Execute curl command + let exitCode = ExternalProcess.execute('curl', args); + + // Check if the request was successful + if (exitCode !== 0) { + printError('Failed to send photo. Curl exit code: ' + exitCode); + } else { + printInfo('Photo successfully sent to Telegram!'); + } +} + +let processAndSendImageToTelegram = function ( + imageWindow, + telegramToken, + telegramChatId, + message, + doStretch, + linked +) { + // Duplicate the image window + var duplicateWindow = duplicateImageWindow(imageWindow, 'TelegramProcessed'); + + // Apply auto-stretch if the flag is set + if (doStretch == true) { + applyAutoStretch(duplicateWindow.mainView, linked); + } + + var width = duplicateWindow.mainView.image.width; + var height = duplicateWindow.mainView.image.height; + var resampleFactor = 1; + + // Loop to resample the image if necessary + while (width / resampleFactor + height / resampleFactor > 10000) { + resampleFactor++; + } + + // Downsample if necessary + if (resampleFactor > 1) { + downsampleImage(duplicateWindow.mainView, -resampleFactor); + } + + // Save the image as JPEG + var jpegPath = saveImageAsJpeg(duplicateWindow, null, 'TelegramProcessed'); + console.noteln('JPEG saved at: ' + jpegPath); + + // Send the image via Telegram + sendImageToTelegram(jpegPath, telegramToken, telegramChatId, message); + + // Close the duplicate window after processing + duplicateWindow.forceClose(); +} + + +// Function to send an iMessage +let sendMessageToIMessage = function (recipient, message) { + if (CoreApplication.platform != "macOS") { + Console.criticalln("iMessage sending is only available on macOS."); + return; + } + + var script = 'tell application "Messages"\n' + + 'set targetService to first service whose service type = iMessage\n' + + 'set targetBuddy to buddy \"' + recipient + '\" of targetService\n' + + 'send \"' + message + '\" to targetBuddy\n' + + 'end tell'; + + console.noteln('script ' + script); + var p = new ExternalProcess(); + p.start("osascript", ["-e", script]); + p.waitForFinished(); + + if (p.exitCode === 0) { + Console.writeln("Message sent successfully."); + } else { + Console.criticalln("Error sending message: " + p.stderr); + } +} + + + +// Function to send image via iMessage +let sendImageToIMessage = function (imagePath, imessageRecipient, message) { + if (!CoreApplication.platform == "macOS") { + printError("iMessage sending is only available on macOS."); + return; + } + + // Define AppleScript for iMessage + var script = + 'tell application "Messages"\n' + + 'set targetService to first service whose service type = iMessage\n' + + 'set targetBuddy to buddy "' + + imessageRecipient + + '" of targetService\n' + + 'send "' + + message + + '" to targetBuddy\n' + + 'send POSIX file "' + + imagePath + + '" to targetBuddy\n' + + 'end tell'; + + console.noteln('script ' + script); + var p = new ExternalProcess(); + p.start("osascript", ["-e", script]); + p.waitForFinished(); + + if (p.exitCode === 0) { + Console.writeln("Message sent successfully."); + } else { + Console.criticalln("Error sending message: " + p.stderr); + } +} + +let processAndSendImageToIMessage = function ( + imageWindow, + imessageRecipient, + message, + doStretch, + linked +){ + // Step 1: Duplicate the image window + var duplicateWindow = duplicateImageWindow(imageWindow, 'foobar23204'); + + // Step 2: Auto-stretch the image (if needed) + if (doStretch) { + applyAutoStretch( + duplicateWindow.mainView, + linked + ); + } + + // Step 3: Save the image as JPEG + var jpegPath = saveImageAsJpeg(duplicateWindow, null, 'iMessageProcessed'); + console.noteln('jpegPath ' + jpegPath); + + // Step 4: Send the image via iMessage + sendImageToIMessage(jpegPath, imessageRecipient, message); + + // Step 5: Close the duplicate window + duplicateWindow.forceClose(); +} + +// Function to send a Pushover message +let sendMessageToPushover = function (userKey, appToken, message) { + var url = "https://api.pushover.net/1/messages.json"; + var args = [ + "-s", "-X", "POST", + url, + "-d", "token=" + appToken, + "-d", "user=" + userKey, + "-d", "message=" + message + ]; + + var p = new ExternalProcess(); + ExternalProcess.execute('curl', args); +} + + + let sendImageToPushover = function (imagePath, userKey, appToken, caption) { + var url = "https://api.pushover.net/1/messages.json"; + var args = [ + "-s", "-X", "POST", + url, + "-F", "token=" + appToken, + "-F", "user=" + userKey, + "-F", "message=" + caption, + "-F", "attachment=@" + imagePath + ]; + console.noteln('imagePath '+imagePath); + + // Execute curl command + let exitCode = ExternalProcess.execute('curl', args); + + // Check if the request was successful + if (exitCode !== 0) { + printError('Failed to send photo. Curl exit code: ' + exitCode); + } else { + printInfo('Photo successfully sent to Pushover!'); + } +} + +let processAndSendImageToPushover = function ( + imageWindow, + userKey, + appToken, + message, + doStretch, + linked +) { + // Duplicate the image window + var duplicateWindow = duplicateImageWindow(imageWindow, 'PushoverProcessed'); + + // Apply auto-stretch if the flag is set + if (doStretch == true) { + applyAutoStretch(duplicateWindow.mainView, linked); + } + + var width = duplicateWindow.mainView.image.width; + var height = duplicateWindow.mainView.image.height; + var resampleFactor = 1; + + // Loop to resample the image if necessary + while (width / resampleFactor + height / resampleFactor > 10000) { + resampleFactor++; + } + + // Downsample if necessary + if (resampleFactor > 1) { + downsampleImage(duplicateWindow.mainView, -resampleFactor); + } + + // Save the image as JPEG + var jpegPath = saveImageAsJpeg(duplicateWindow, null, 'PushoverProcessed'); + console.noteln('JPEG saved at: ' + jpegPath); + + // Send the image + sendImageToPushover(jpegPath, userKey, appToken, message); + + // Close the duplicate window after processing + duplicateWindow.forceClose(); +} + + +let sendImage = function ( + targetWindow, + platform, + telegramToken, + telegramChatId, + imessageRecipient, + pushoverUser, + pushoverToken, + message, + doStretch, + linked +) { + if (platform === PLATFORMS.TELEGRAM) { + // For Telegram, use telegramToken and telegramChatId + processAndSendImageToTelegram( + targetWindow, + telegramToken, // Telegram Bot Token + telegramChatId, // Telegram Chat ID + message, // Message text (shared for both platforms) + doStretch, // Pass doStretch + linked // Pass linked + ); + } else if (platform === PLATFORMS.IMESSAGE) { + // For iMessage, use imessageRecipient + processAndSendImageToIMessage( + targetWindow, + imessageRecipient, // iMessage recipient + message, // Message text (shared for both platforms) + doStretch, // Pass doStretch + linked // Pass linked + ); + } else if (platform === PLATFORMS.PUSHOVER) { + processAndSendImageToPushover( + targetWindow, + pushoverUser, + pushoverToken, + message, + doStretch, + linked + ); + } + else { + printError('Unknown platform!'); + } +} + +//////////////////////////////////////////////////////////////////////////// +//@end of temporary redecleration of contents of SendMessageToPhone.js - to be removed once I know how to import them +//////////////////////////////////////////////////////////////////////////// + +let sendEventScriptMessage = function (message) { +sendMessage( + savedConfig.platform, + savedConfig.telegramBotToken, + savedConfig.telegramChatId.toNumber(), + savedConfig.imessageRecipient, + savedConfig.pushoverUser, + savedConfig.pushoverToken, + message +) +}; + +// Function to process, autostretch, and send the stacked image +let sendStackedImage = function (imageToSend, caption) { + // Step 1: Open the stacked image + let [window] = ImageWindow.open(imageToSend); + if (!window) { + console.criticalln('Failed to open the stacked image.'); + return; + } + sendImage( + window, + savedConfig.platform, + savedConfig.telegramBotToken, + savedConfig.telegramChatId, + savedConfig.imessageRecipient, + savedConfig.pushoverUser, + savedConfig.pushoverToken, + caption, + true, + false + ); +} + + +if (env.event == 'start') { + if (env.group) { + let totalFramesCount = env.group.fileItems.length; + let activeFramesCount = env.group.activeFrames().length; + + sendEventScriptMessage( + env.name + + ' starting: processing ' + + activeFramesCount + + '/' + + totalFramesCount + + ' active frames' + ); + } else { + sendEventScriptMessage(env.name); + + } +} + +if (env.event == 'done') { + if (env.status == OperationBlockStatus.DONE) { + sendEventScriptMessage(env.name + ' successfully executed'); + } else if (env.status == OperationBlockStatus.FAILED) { + sendEventScriptMessage(env.name + ' failed, ' + env.statusMessage); + } +} + +// Ensure env exists before accessing properties +if (typeof env !== 'undefined' && env.name) { + if ( + (env.name == 'Integration' || env.name == 'FastIntegration') && + env.event == 'done' + ) { + sendStackedImage( + env.group.getMasterFileName( + WBPPMasterType.MASTER_LIGHT, + WBPPMasterVariant.REGULAR + ), + 'Your integrated Master Light file is here!' + ); + } else if (env.name == 'Drizzle Integration' && env.event == 'done') { + sendStackedImage( + env.group.getMasterFileName( + WBPPMasterType.MASTER_LIGHT, + WBPPMasterVariant.DRIZZLE + ), + 'Your integrated Drizzle file is here!' + ); + } else if (env.name.indexOf('onPostProcessEnd') == 0 && env.event == 'done') { + customFinalSteps = true; + } +} +if (env.event == 'pipeline start') { + sendEventScriptMessage('WBPP started.'); +} + +if (env.event === 'pipeline end') { + sendEventScriptMessage('WBPP terminated.'); +} diff --git a/pixinsight_update_bundle/src/scripts/TheSpaceKoala/SendImageToTelegram.js b/pixinsight_update_bundle/src/scripts/TheSpaceKoala/SendImageToTelegram.js new file mode 100644 index 0000000..8d22c91 --- /dev/null +++ b/pixinsight_update_bundle/src/scripts/TheSpaceKoala/SendImageToTelegram.js @@ -0,0 +1,439 @@ +// ================================================================================== +// SendImageToTelegram.js - A PixInsight script to process an image and send it to Telegram +// +// Copyright (c) 2025 Luca Bartek +// Notification Event Script +// Version: V1.0.1 - +// Author: Luca Bartek +// Website: www.thespacekoala.com +// +// Redistribution and use in both source and binary forms, with or without +// modification, is permitted provided that the following conditions are met: +// +// 1. All redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// +// 2. All redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// ================================================================================== + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "KoalaUtils.js" + +#define TITLE "Send Image To Telegram" +#define VERSION "V1.0.8" // 20250505 + +this.TELEGRAM_SETTINGS_KEY_BASE = 'SendImageToTelegram/'; + +function loadTelegramSettings() { + var telegramBotToken = Settings.read( + TELEGRAM_SETTINGS_KEY_BASE + 'telegramBotToken', + DataType_UCString + ); + var chatID = Settings.read( + TELEGRAM_SETTINGS_KEY_BASE + 'chatID', + DataType_UCString + ); + if (chatID == -2147483648) { + chatID = Number.NaN; + } + var doStretch = Settings.read( + TELEGRAM_SETTINGS_KEY_BASE + 'doStretch', + DataType_Boolean + ); + var linked = Settings.read( + TELEGRAM_SETTINGS_KEY_BASE + 'linked', + DataType_Boolean + ); + var message = + Settings.read(TELEGRAM_SETTINGS_KEY_BASE + 'message', DataType_UCString) || + ''; + + return { + telegramBotToken: telegramBotToken, + chatID: chatID, + doStretch: doStretch, + linked: linked, + message: message, + }; +} + +function resetTelegramSettings() { + Settings.remove(TELEGRAM_SETTINGS_KEY_BASE + 'telegramBotToken'); + Settings.remove(TELEGRAM_SETTINGS_KEY_BASE + 'chatID'); + Settings.remove(TELEGRAM_SETTINGS_KEY_BASE + 'doStretch'); + Settings.remove(TELEGRAM_SETTINGS_KEY_BASE + 'linked'); + Settings.remove(TELEGRAM_SETTINGS_KEY_BASE + 'message'); +} + +function saveTelegramSettings( + telegramBotToken, + chatID, + doStretch, + linked, + message +) { + Settings.write( + TELEGRAM_SETTINGS_KEY_BASE + 'telegramBotToken', + DataType_UCString, + telegramBotToken + ); + Settings.write(TELEGRAM_SETTINGS_KEY_BASE + 'chatID', DataType_UCString, chatID.toString()); + Settings.write( + TELEGRAM_SETTINGS_KEY_BASE + 'doStretch', + DataType_Boolean, + doStretch + ); + Settings.write( + TELEGRAM_SETTINGS_KEY_BASE + 'linked', + DataType_Boolean, + linked + ); + Settings.write( + TELEGRAM_SETTINGS_KEY_BASE + 'message', + DataType_UCString, + message + ); +} + +function validateInputs(token, chatID) { + if (!token) { + raiseError('Telegram Bot Token is mandatory.'); + return false; + } + if (token.startsWith('bot')) { + raiseError('Insert the Bot Token without "bot"'); + return false; + } + if (!chatID || isNaN(chatID)) { + raiseError('Chat ID is mandatory and must be a valid number.'); + return false; + } + return true; +} + +function sendPhotoToTelegram(imagePath, telegramToken, chatId, message) { + const url = 'https://api.telegram.org/bot' + telegramToken + '/sendPhoto'; + let args = [ + '-s', + '-X', + 'POST', + url, + '-F', + 'chat_id=' + chatId, + '-F', + 'photo=@' + imagePath, + ]; + if (message) { + args[args.length] = '-F'; + args[args.length] = 'caption=' + message; + } +console.noteln('args ' + args); + + // Execute curl command + let exitCode = ExternalProcess.execute('curl', args); + + // Check if the request was successful + if (exitCode !== 0) { + printError('Failed to send photo. Curl exit code: ' + exitCode); + } else { + printInfo('Photo successfully sent to Telegram!'); + } +} + +function processAndSendImageToTelegram( + imageWindow, + telegramToken, + chatId, + doStretch, + linked, + message +) { + // Duplicate the image window + var duplicateWindow = duplicateImageWindow(imageWindow, 'foobar23204'); + + if (doStretch) { + applyAutoStretch( + duplicateWindow.mainView, + linked && duplicateWindow.mainView.image.isColor + ); + } + + var width = duplicateWindow.mainView.image.width; + var height = duplicateWindow.mainView.image.height; + var resampleFactor = 1; + + // Loop until width + height is below 10,000 + while (width / resampleFactor + height / resampleFactor > 10000) { + resampleFactor++; + } + if (resampleFactor > 1) { + downsampleImage(duplicateWindow.mainView, -resampleFactor); + } + + // Export image as JPEG + var jpegPath = saveImageAsJpeg(duplicateWindow, null, 'TelegramProcessed'); + console.noteln('jpegPath ' + jpegPath); + + // Send the image via Telegram + sendPhotoToTelegram(jpegPath, telegramToken, chatId, message); + // Close the duplicate window + duplicateWindow.forceClose(); +} + +function TelegramDialog() { + this.__base__ = Dialog; + this.__base__(); + + this.windowTitle = 'Send Image to Telegram'; + // Load saved settings + let savedConfig = loadTelegramSettings(); + // Telegram Token Input + this.tokenLabel = new Label(this); + this.tokenLabel.text = 'Telegram Bot Token:'; + this.tokenLabel.textAlignment = TextAlign_Right | TextAlign_VertCenter; + + this.tokenInput = new Edit(this); + this.tokenInput.text = savedConfig.telegramBotToken || ''; + this.tokenInput.setFixedWidth(350); + + let tokenSizer = new HorizontalSizer(); + tokenSizer.spacing = 4; + tokenSizer.add(this.tokenLabel); + tokenSizer.add(this.tokenInput, 100); + + // Chat ID Input + this.chatIdLabel = new Label(this); + this.chatIdLabel.text = 'Chat ID:'; + this.chatIdLabel.textAlignment = TextAlign_Right | TextAlign_VertCenter; + + this.chatIdInput = new NumericEdit(this); + this.chatIdInput.setRange( + -99999999999999999999999999, + 99999999999999999999999999 + ); // Set a reasonable range + this.chatIdInput.setPrecision(0); // No decimals + this.chatIdInput.setValue(savedConfig.chatID.toNumber() || Number.NaN); + this.chatIdInput.setFixedWidth(350); + + let chatIdSizer = new HorizontalSizer(); + chatIdSizer.spacing = 4; + chatIdSizer.add(this.chatIdLabel); + chatIdSizer.add(this.chatIdInput, 100); + + // === Message Input === + this.messageLabel = new Label(this); + this.messageLabel.text = 'Message:'; + this.messageLabel.textAlignment = TextAlign_Right | TextAlign_VertCenter; + + this.messageInput = new Edit(this); + this.messageInput.text = savedConfig.message || ''; // Load saved message + this.messageInput.setFixedWidth(350); + + var messageSizer = new HorizontalSizer(); + messageSizer.spacing = 4; + messageSizer.add(this.messageLabel); + messageSizer.add(this.messageInput, 100); + + // === Auto-Stretch Checkbox === + this.autoStretchCheckbox = new CheckBox(this); + this.autoStretchCheckbox.text = 'Auto-Stretch'; + this.autoStretchCheckbox.checked = savedConfig.doStretch || false; + + var autoStretchSizer = new HorizontalSizer(); + autoStretchSizer.spacing = 4; + autoStretchSizer.addSpacing(20); + autoStretchSizer.add(this.autoStretchCheckbox); + autoStretchSizer.addStretch(); + + // === Linked Checkbox (Only Active When Auto-Stretch is Enabled) === + this.linkedCheckbox = new CheckBox(this); + this.linkedCheckbox.text = 'Linked Channels'; + this.linkedCheckbox.checked = savedConfig.linked || false; + this.linkedCheckbox.enabled = this.autoStretchCheckbox.checked; // Initially enabled based on auto-stretch + + var linkedSizer = new HorizontalSizer(); + linkedSizer.spacing = 4; + linkedSizer.addSpacing(20); + linkedSizer.add(this.linkedCheckbox); + linkedSizer.addStretch(); + + // === Toggle Linked Checkbox Based on Auto-Stretch === + var self = this; + this.autoStretchCheckbox.onCheck = function (checked) { + self.linkedCheckbox.enabled = checked; + }; + + // === "New Instance" Button (Triangle Icon) === + this.newInstanceButton = new ToolButton(this); + this.newInstanceButton.icon = this.scaledResource( + ':/process-interface/new-instance.png' + ); + this.newInstanceButton.setScaledFixedSize(24, 24); + this.newInstanceButton.toolTip = 'New Instance'; + var self = this; + this.newInstanceButton.onMousePress = function () { + var P = new Object(); + + // Save all parameters into P + P.telegramBotToken = self.tokenInput.text.trim(); + P.chatID = self.chatIdInput.value; + P.doStretch = self.autoStretchCheckbox.checked; + P.linked = self.linkedCheckbox.checked; + P.message = self.messageInput.text.trim(); + + // Store parameters in the instance + Parameters.clear(); + for (var key in P) { + if (P[key] !== undefined) { + Parameters.set(key, P[key]); + } + } + self.newInstance(); + }; + + // === "Documentation Browser" Button (Docm Icon) === + + this.browseDocumentationButton = new ToolButton(this); + this.browseDocumentationButton.icon = this.scaledResource(":/process-interface/browse-documentation.png"); + this.browseDocumentationButton.setScaledFixedSize(24, 24); + this.browseDocumentationButton.toolTip = + "

Opens a browser to view the script's documentation.

"; + this.browseDocumentationButton.onClick = function () { + Dialog.browseScriptDocumentation("SendImageToTelegram"); + } + + + // Run Button + this.runButton = new PushButton(this); + this.runButton.text = 'Run'; + this.runButton.icon = this.scaledResource(':/icons/power.png'); + + this.runButton.onClick = () => { + var token = this.tokenInput.text.trim(); + var chatID = this.chatIdInput.value; + var doStretch = this.autoStretchCheckbox.checked; + var linked = this.linkedCheckbox.checked; + var message = this.messageInput.text.trim(); + if (!validateInputs(token, chatID)) { + return; // Exit if validation fails + } + + // Execute the image processing and sending function + if (!ImageWindow.activeWindow.isNull) { + processAndSendImageToTelegram( + ImageWindow.activeWindow, + token, + chatID, + doStretch, + linked, + message + ); + saveTelegramSettings(token, chatID, doStretch, linked, message); // Save settings + this.ok(); // Close the log + } else { + raiseError('No active image window found'); + } + }; + + // Cancel Button + this.cancelButton = new PushButton(this); + this.cancelButton.text = 'Cancel'; + this.cancelButton.icon = this.scaledResource(':/icons/close.png'); + this.cancelButton.onClick = function () { + this.dialog.cancel(); + }; + + // Reset Button + this.resetButton = new ToolButton(this); + this.resetButton.icon = this.scaledResource(':/process-interface/reset.png'); + this.resetButton.setScaledFixedSize(24, 24); + this.resetButton.toolTip = '

Resets all settings to their defaults.

'; + this.resetButton.onClick = () => { + resetTelegramSettings(); + this.tokenInput.text = ''; + this.chatIdInput.setValue(Number.NaN); + this.autoStretchCheckbox.checked = false; + this.linkedCheckbox.checked = false; + this.messageInput.text = ''; + }; + + // Button Layout + var buttonSizer = new HorizontalSizer(); + buttonSizer.spacing = 8; + buttonSizer.add(this.newInstanceButton); + buttonSizer.add(this.browseDocumentationButton); + buttonSizer.addStretch(); + buttonSizer.add(this.runButton); + buttonSizer.add(this.cancelButton); + buttonSizer.add(this.resetButton); + + // Main Layout + this.sizer = new VerticalSizer(); + this.sizer.margin = 10; + this.sizer.spacing = 6; + this.sizer.add(tokenSizer); + this.sizer.add(chatIdSizer); + this.sizer.add(messageSizer); + this.sizer.add(autoStretchSizer); + this.sizer.add(linkedSizer); + this.sizer.addSpacing(8); + this.sizer.add(buttonSizer); + + this.adjustToContents(); +} + +function main() { + printTheSpaceKoalaBanner(); + + // If the process was dragged onto an image, execute immediately without opening the GUI + if (Parameters.isViewTarget) { + let targetWindow = Parameters.targetView.window; + + if (!targetWindow.isNull) { + var token = Parameters.get('telegramBotToken'); + var chatID = Parameters.get('chatID'); + var doStretch = Parameters.get('doStretch'); + var linked = Parameters.get('linked'); + var message = Parameters.get('message'); + + if (!validateInputs(token, chatID)) { + return; // Exit if validation fails + } + + processAndSendImageToTelegram( + targetWindow, + token, + chatID, + doStretch, + linked, + message + ); + } else { + printError('No valid target image found.'); + } + return; + } + + // Otherwise: Open the GUI + let dialog = new TelegramDialog(); + if (!dialog.execute()) { + printInfo('User canceled the script.'); + return; + } +} + +// Extend Dialog prototype +TelegramDialog.prototype = new Dialog(); + +main(); diff --git a/pixinsight_update_bundle/src/scripts/TheSpaceKoala/SendMessageToIMessage.js b/pixinsight_update_bundle/src/scripts/TheSpaceKoala/SendMessageToIMessage.js new file mode 100644 index 0000000..0c702af --- /dev/null +++ b/pixinsight_update_bundle/src/scripts/TheSpaceKoala/SendMessageToIMessage.js @@ -0,0 +1,106 @@ +// ================================================================================== +// SendMessageToIMessage.js +// +// Copyright (c) 2025 Luca Bartek +// Notification Event Script +// Version: V1.0.2 //20250521 +// Author: Luca Bartek +// Website: www.thespacekoala.com +// +// Redistribution and use in both source and binary forms, with or without +// modification, is permitted provided that the following conditions are met: +// +// 1. All redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// +// 2. All redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// ================================================================================== + +//#include "KoalaUtils.js" + + +// Function to send an iMessage +function sendMessageToIMessage(recipient, message) { + if (CoreApplication.platform != "macOS") { + Console.criticalln("iMessage sending is only available on macOS."); + return; + } + + var script = 'tell application "Messages"\n' + + 'set targetService to first service whose service type = iMessage\n' + + 'set targetBuddy to buddy \"' + recipient + '\" of targetService\n' + + 'send \"' + message + '\" to targetBuddy\n' + + 'end tell'; + + var p = new ExternalProcess(); + p.start("osascript", ["-e", script]); + p.waitForFinished(); + + if (p.exitCode === 0) { + Console.writeln("✅ Message sent successfully."); + } else { + Console.criticalln("❌ Error sending message: " + p.stderr); + } +} + + +// Function to send image via iMessage +function sendImageToIMessage(imagePath, imessageRecipient, message) { + if (!CoreApplication.platform == "macOS") { + printError("iMessage sending is only available on macOS."); + return; + } + + // Define AppleScript for iMessage + var script = + 'tell application "Messages"\n' + + 'set targetService to first service whose service type = iMessage\n' + + 'set targetBuddy to buddy "' + + imessageRecipient + + '" of targetService\n' + + 'send "' + + message + + '" to targetBuddy\n' + + 'send POSIX file "' + + imagePath + + '" to targetBuddy\n' + + 'end tell'; + + var p = new ExternalProcess(); + p.start("osascript", ["-e", script]); + p.waitForFinished(); + + if (p.exitCode === 0) { + Console.writeln("✅ Message sent successfully."); + } else { + Console.criticalln("❌ Error sending message: " + p.stderr); + } +} + +function processAndSendImageToIMessage( + imageWindow, + imessageRecipient, + message, + doStretch, + linked +) { + // Step 1: Duplicate the image window + var duplicateWindow = duplicateImageWindow(imageWindow, 'foobar23204'); +// Step 2: Auto-stretch the image (if needed) + if (doStretch === true) { + applyAutoStretch( + duplicateWindow.mainView ); + } + + // Step 3: Save the image as JPEG + var jpegPath = saveImageAsJpeg(duplicateWindow, null, 'iMessageProcessed'); + + // Step 4: Send the image via iMessage + sendImageToIMessage(jpegPath, imessageRecipient, message); + + // Step 5: Close the duplicate window + duplicateWindow.forceClose(); +} diff --git a/pixinsight_update_bundle/src/scripts/TheSpaceKoala/SendMessageToPhone.js b/pixinsight_update_bundle/src/scripts/TheSpaceKoala/SendMessageToPhone.js new file mode 100644 index 0000000..982fcba --- /dev/null +++ b/pixinsight_update_bundle/src/scripts/TheSpaceKoala/SendMessageToPhone.js @@ -0,0 +1,639 @@ +// ================================================================================== +// SendMessageToPhone.js - A PixInsight script to process an image and send it to Telegram or iMessage +// +// Copyright (c) 2025 Luca Bartek +// +// Send Message To Phone +// Author: Luca Bartek +// Website: www.thespacekoala.com +// +// Redistribution and use in both source and binary forms, with or without +// modification, is permitted provided that the following conditions are met: +// +// 1. All redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// +// 2. All redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// ================================================================================== + +#feature-id SendMessageToPhone : TheSpaceKoala > SendImageToPhone +#feature-icon messageIcon.svg +#feature-info This script processes the active image and sends it to your phone via the selected service. +#include +#include +#include +#include +#include +#include +#include +#include "KoalaUtils.js" +#include "SendMessageToIMessage.js" +#include "SendMessageToTelegram.js" +#include "SendMessageToPushover.js" + + +#define TITLE "Send Image To Phone" +#define VERSION "V1.0.3" //20250520 + + +var PLATFORMS = { + TELEGRAM: 1, + IMESSAGE: 2, + PUSHOVER: 3, +}; + +var SendMessageToPhoneConstants = SendMessageToPhoneConstants || {}; + +SendMessageToPhoneConstants.PHONE_PLATFORM_KEY = 'SendMessageToPhone/platform'; +SendMessageToPhoneConstants.TELEGRAM_TOKEN_KEY = 'SendMessageToPhone/telegramBotToken'; +SendMessageToPhoneConstants.TELEGRAM_CHAT_ID_KEY = 'SendMessageToPhone/telegramChatId'; +SendMessageToPhoneConstants.MESSAGE_KEY = 'SendMessageToPhone/message'; +SendMessageToPhoneConstants.IMESSAGE_RECIPIENT_KEY = 'SendMessageToPhone/imessageRecipient'; +SendMessageToPhoneConstants.PUSHOVER_USER_KEY = 'SendMessageToPhone/pushoverUser'; +SendMessageToPhoneConstants.PUSHOVER_TOKEN_KEY = 'SendMessageToPhone/pushoverToken'; +SendMessageToPhoneConstants.DO_STRETCH_KEY = 'SendMessageToPhone/doStretch'; +SendMessageToPhoneConstants.LINKED_KEY = 'SendMessageToPhone/linked'; + +function loadPhoneSettings() { + return { + platform: + Settings.read( + SendMessageToPhoneConstants.PHONE_PLATFORM_KEY, + DataType_Int32 + ) || PLATFORMS.TELEGRAM, + telegramBotToken: + Settings.read( + SendMessageToPhoneConstants.TELEGRAM_TOKEN_KEY, + DataType_UCString + ) || '', + telegramChatId: (function () { + var v = Settings.read( + SendMessageToPhoneConstants.TELEGRAM_CHAT_ID_KEY, + DataType_UCString + ); + return typeof v === 'undefined' ? 0 : v; + })(), + message: + Settings.read( + SendMessageToPhoneConstants.MESSAGE_KEY, + DataType_UCString + ) || ':)', + imessageRecipient: + Settings.read( + SendMessageToPhoneConstants.IMESSAGE_RECIPIENT_KEY, + DataType_UCString + ) || '', + pushoverUser: + Settings.read( + SendMessageToPhoneConstants.PUSHOVER_USER_KEY, + DataType_UCString + ) || '', + pushoverToken: + Settings.read( + SendMessageToPhoneConstants.PUSHOVER_TOKEN_KEY, + DataType_UCString + ) || '', + }; +} + +function savePhoneSettings(data) { + // Save the platform, Telegram credentials, iMessage recipient, message, doStretch, and linked settings + Settings.write(SendMessageToPhoneConstants.PHONE_PLATFORM_KEY, DataType_Int32, data.platform); + Settings.write(SendMessageToPhoneConstants.TELEGRAM_TOKEN_KEY, DataType_UCString, data.telegramBotToken); + Settings.write(SendMessageToPhoneConstants.TELEGRAM_CHAT_ID_KEY, DataType_UCString, data.telegramChatId.toString()); + Settings.write(SendMessageToPhoneConstants.MESSAGE_KEY, DataType_UCString, data.message); + Settings.write(SendMessageToPhoneConstants.IMESSAGE_RECIPIENT_KEY, DataType_UCString, data.imessageRecipient); + Settings.write(SendMessageToPhoneConstants.DO_STRETCH_KEY, DataType_Boolean, data.doStretch); + Settings.write(SendMessageToPhoneConstants.LINKED_KEY, DataType_Boolean, data.linked); + Settings.write(SendMessageToPhoneConstants.PUSHOVER_USER_KEY, DataType_UCString, data.pushoverUser); + Settings.write(SendMessageToPhoneConstants.PUSHOVER_TOKEN_KEY, DataType_UCString, data.pushoverToken); +} + +function resetPhoneSettings() { + // Reset saved settings to their default values + Settings.remove(SendMessageToPhoneConstants.PHONE_PLATFORM_KEY); + Settings.remove(SendMessageToPhoneConstants.TELEGRAM_TOKEN_KEY); + Settings.remove(SendMessageToPhoneConstants.TELEGRAM_CHAT_ID_KEY); + Settings.remove(SendMessageToPhoneConstants.MESSAGE_KEY); + Settings.remove(SendMessageToPhoneConstants.IMESSAGE_RECIPIENT_KEY); + Settings.remove(SendMessageToPhoneConstants.DO_STRETCH_KEY); + Settings.remove(SendMessageToPhoneConstants.LINKED_KEY); + Settings.remove(SendMessageToPhoneConstants.PUSHOVER_USER_KEY); + Settings.remove(SendMessageToPhoneConstants.PUSHOVER_TOKEN_KEY); +} + +function sendMessage( + platform, + telegramToken, + telegramChatId, + imessageRecipient, + pushoverUser, + pushoverToken, + message +) { + + if (platform === PLATFORMS.TELEGRAM) { + sendMessageToTelegram( + telegramToken, + telegramChatId, + message + ); + } else if (platform === PLATFORMS.IMESSAGE) { + sendMessageToIMessage( + imessageRecipient, + message + ); + } else if (platform === PLATFORMS.PUSHOVER) { + sendMessageToPushover( + pushoverUser, + pushoverToken, + message + ); + } else { + printError('Please configure platform in the SendMessageToPhone script'); + } +} + +function sendImage( + targetWindow, + platform, + telegramToken, + telegramChatId, + imessageRecipient, + pushoverUser, + pushoverToken, + message, + doStretch, + linked +) { + + if (platform === PLATFORMS.TELEGRAM) { + processAndSendImageToTelegram( + targetWindow, + telegramToken, + telegramChatId, + message, + doStretch, + linked + ); + } else if (platform === PLATFORMS.IMESSAGE) { + processAndSendImageToIMessage( + targetWindow, + imessageRecipient, + message, + doStretch, + linked + ); + } else if (platform === PLATFORMS.PUSHOVER) { + processAndSendImageToPushover( + targetWindow, + pushoverUser, + pushoverToken, + message, + doStretch, + linked + ); + } else { + printError('Unknown platform!'); + } +} +// ==== Main Dialog ==== + +function SendMessageToPhoneDialog() { + this.__base__ = Dialog; + this.__base__(); + + this.windowTitle = 'Send Message to Phone'; + + var self = this; + var labelWidth = this.logicalPixelsToPhysical(120); + + // Load saved settings + var savedConfig = loadPhoneSettings(); + + this.sizer = new VerticalSizer(); + this.sizer.margin = 10; + this.sizer.spacing = 6; + + // === Platform Selection === + this.platformGroupBox = new GroupBox(this); + this.platformGroupBox.title = 'Choose Platform'; + this.platformGroupBox.sizer = new VerticalSizer(); + this.platformGroupBox.sizer.margin = 6; + this.platformGroupBox.sizer.spacing = 4; + + this.telegramRadio = new RadioButton(this.platformGroupBox); + this.telegramRadio.text = 'Telegram'; + this.telegramRadio.checked = savedConfig.platform === PLATFORMS.TELEGRAM; + + this.imessageRadio = new RadioButton(this.platformGroupBox); + this.imessageRadio.text = 'iMessage'; + this.imessageRadio.checked = savedConfig.platform === PLATFORMS.IMESSAGE; + + this.pushoverRadio = new RadioButton(this.platformGroupBox); + this.pushoverRadio.text = 'Pushover'; + this.pushoverRadio.checked = savedConfig.platform === PLATFORMS.PUSHOVER; + + if (CoreApplication.platform != 'macOS') { + this.imessageRadio.enabled = false; + } + + this.platformGroupBox.sizer.add(this.telegramRadio); + this.platformGroupBox.sizer.add(this.imessageRadio); + this.platformGroupBox.sizer.add(this.pushoverRadio); + this.sizer.add(this.platformGroupBox); + + // === Telegram Configuration === + this.telegramGroupBox = new GroupBox(this); + this.telegramGroupBox.title = 'Telegram Configuration'; + this.telegramGroupBox.sizer = new VerticalSizer(); + this.telegramGroupBox.sizer.margin = 6; + this.telegramGroupBox.sizer.spacing = 4; + + var telegramTokenSizer = new HorizontalSizer(); + this.telegramTokenLabel = new Label(this.telegramGroupBox); + this.telegramTokenLabel.text = 'Bot Token:'; + this.telegramTokenLabel.minWidth = labelWidth; + this.telegramTokenLabel.textAlignment = + TextAlign_Right | TextAlign_VertCenter; + + this.telegramTokenInput = new Edit(this.telegramGroupBox); + this.telegramTokenInput.text = savedConfig.telegramBotToken; + + telegramTokenSizer.spacing = 4; + telegramTokenSizer.add(this.telegramTokenLabel); + telegramTokenSizer.add(this.telegramTokenInput, 100); + this.telegramGroupBox.sizer.add(telegramTokenSizer); + + var telegramChatIdSizer = new HorizontalSizer(); + this.telegramChatIdLabel = new Label(this.telegramGroupBox); + this.telegramChatIdLabel.text = 'Chat ID:'; + this.telegramChatIdLabel.minWidth = labelWidth; + this.telegramChatIdLabel.textAlignment = + TextAlign_Right | TextAlign_VertCenter; + + this.telegramChatIdInput = new NumericEdit(this.telegramGroupBox); + this.telegramChatIdInput.setRange( + -9223372036854775808, + 9223372036854775807 + ); + this.telegramChatIdInput.setPrecision(0); + if (typeof savedConfig.telegramChatId !== 'undefined' && savedConfig.telegramChatId !== null) { + this.telegramChatIdInput.setValue(savedConfig.telegramChatId.toNumber()); + } else { + this.telegramChatIdInput.setValue(Number.NaN); + } + + // Fix the alignment: hide the internal label and set width + this.telegramChatIdInput.label.visible = false; + this.telegramChatIdInput.setFixedWidth(150); // Align width with other fields + + telegramChatIdSizer.spacing = 4; + telegramChatIdSizer.add(this.telegramChatIdLabel); + telegramChatIdSizer.add(this.telegramChatIdInput, 100); + this.telegramGroupBox.sizer.add(telegramChatIdSizer); + + this.sizer.add(this.telegramGroupBox); + + +// === iMessage Configuration === +this.imessageGroupBox = new GroupBox(this); +this.imessageGroupBox.title = 'iMessage Configuration'; +this.imessageGroupBox.sizer = new VerticalSizer(); +this.imessageGroupBox.sizer.margin = 6; +this.imessageGroupBox.sizer.spacing = 4; + +var imessageRecipientSizer = new HorizontalSizer(); +this.imessageRecipientLabel = new Label(this.imessageGroupBox); +this.imessageRecipientLabel.text = 'Recipient:'; +this.imessageRecipientLabel.minWidth = labelWidth; +this.imessageRecipientLabel.textAlignment = + TextAlign_Right | TextAlign_VertCenter; + +this.imessageRecipientInput = new Edit(this.imessageGroupBox); +this.imessageRecipientInput.text = savedConfig.imessageRecipient; + +imessageRecipientSizer.spacing = 4; +imessageRecipientSizer.add(this.imessageRecipientLabel); +imessageRecipientSizer.add(this.imessageRecipientInput, 100); +this.imessageGroupBox.sizer.add(imessageRecipientSizer); + +// === Message Field === +this.messageGroupBox = new GroupBox(this); +this.messageGroupBox.title = 'Message'; +this.messageGroupBox.sizer = new VerticalSizer(); +this.messageGroupBox.sizer.margin = 6; +this.messageGroupBox.sizer.spacing = 6; // Increased spacing + +var messageSizer = new HorizontalSizer(); +this.messageLabel = new Label(this.messageGroupBox); +this.messageLabel.text = 'Message Text:'; +this.messageLabel.minWidth = labelWidth; +this.messageLabel.textAlignment = TextAlign_Right | TextAlign_VertCenter; + +this.messageInput = new Edit(this.messageGroupBox); +this.messageInput.text = savedConfig.message; + +messageSizer.spacing = 4; +messageSizer.add(this.messageLabel); +messageSizer.add(this.messageInput, 100); +this.messageGroupBox.sizer.add(messageSizer); + +// === Auto-Stretch Checkbox (Inside Message Group Box) === +this.doStretchCheckbox = new CheckBox(this.messageGroupBox); +this.doStretchCheckbox.text = 'Auto-Stretch'; +this.doStretchCheckbox.checked = savedConfig.doStretch || false; + +var autoStretchSizer = new HorizontalSizer(); +autoStretchSizer.spacing = 4; +autoStretchSizer.addSpacing(20); +autoStretchSizer.add(this.doStretchCheckbox); +autoStretchSizer.addStretch(); + +// === Linked Checkbox (Only Active When Auto-Stretch is Enabled) === +this.linkedCheckbox = new CheckBox(this.messageGroupBox); +this.linkedCheckbox.text = 'Linked Channels'; +this.linkedCheckbox.checked = savedConfig.linked || false; +this.linkedCheckbox.enabled = this.doStretchCheckbox.checked; // Initially enabled based on auto-stretch + +var linkedSizer = new HorizontalSizer(); +linkedSizer.spacing = 4; +linkedSizer.addSpacing(20); +linkedSizer.add(this.linkedCheckbox); +linkedSizer.addStretch(); + +// === Toggle Linked Checkbox Based on Auto-Stretch === +var self = this; +this.doStretchCheckbox.onCheck = function (checked) { + self.linkedCheckbox.enabled = checked; +}; + +// Add the checkboxes to the Message Group Box with better spacing +this.messageGroupBox.sizer.add(autoStretchSizer); +this.messageGroupBox.sizer.add(linkedSizer); + +// Add the message group box to the main sizer +this.sizer.add(this.imessageGroupBox); + +// === Pushover Configuration === +this.pushoverGroupBox = new GroupBox(this); +this.pushoverGroupBox.title = 'Pushover Configuration'; +this.pushoverGroupBox.sizer = new VerticalSizer(); +this.pushoverGroupBox.sizer.margin = 6; +this.pushoverGroupBox.sizer.spacing = 4; + +var pushoverUserSizer = new HorizontalSizer(); +this.pushoverUserLabel = new Label(this.pushoverGroupBox); +this.pushoverUserLabel.text = 'User Key:'; +this.pushoverUserLabel.minWidth = labelWidth; +this.pushoverUserLabel.textAlignment = TextAlign_Right | TextAlign_VertCenter; + +this.pushoverUserInput = new Edit(this.pushoverGroupBox); +this.pushoverUserInput.text = savedConfig.pushoverUser; + +pushoverUserSizer.spacing = 4; +pushoverUserSizer.add(this.pushoverUserLabel); +pushoverUserSizer.add(this.pushoverUserInput, 100); +this.pushoverGroupBox.sizer.add(pushoverUserSizer); + +var pushoverTokenSizer = new HorizontalSizer(); +this.pushoverTokenLabel = new Label(this.pushoverGroupBox); +this.pushoverTokenLabel.text = 'App Token:'; +this.pushoverTokenLabel.minWidth = labelWidth; +this.pushoverTokenLabel.textAlignment = TextAlign_Right | TextAlign_VertCenter; + +this.pushoverTokenInput = new Edit(this.pushoverGroupBox); +this.pushoverTokenInput.text = savedConfig.pushoverToken; + +pushoverTokenSizer.spacing = 4; +pushoverTokenSizer.add(this.pushoverTokenLabel); +pushoverTokenSizer.add(this.pushoverTokenInput, 100); +this.pushoverGroupBox.sizer.add(pushoverTokenSizer); + +this.sizer.add(this.pushoverGroupBox); + +this.sizer.add(this.messageGroupBox); + + // === Buttons === + + // === New Instance Button === + this.newInstanceButton = new ToolButton(this); + this.newInstanceButton.icon = this.scaledResource( + ':/process-interface/new-instance.png' + ); // Set the triangle icon + this.newInstanceButton.setScaledFixedSize(24, 24); // Set the size + this.newInstanceButton.toolTip = 'New Instance'; // Tooltip text + + this.runButton = new PushButton(this); + this.runButton.text = 'Send'; + this.runButton.icon = this.scaledResource(':/icons/power.png'); + + this.cancelButton = new PushButton(this); + this.cancelButton.text = 'Cancel'; + this.cancelButton.icon = this.scaledResource(':/icons/close.png'); + + this.saveParamsButton = new PushButton(this); // Save Parameters button + this.saveParamsButton.text = 'Save Parameters'; // Set button text + this.saveParamsButton.icon = this.scaledResource( + ':/process-interface/save.png' + ); // Set icon + +// === Reset Parameters Button === +this.resetButton = new PushButton(this); +this.resetButton.text = 'Reset Parameters'; +this.resetButton.icon = this.scaledResource(':/process-interface/reset.png'); // Add an icon if needed + + + var buttonSizer = new HorizontalSizer(); + buttonSizer.spacing = 8; + buttonSizer.add(this.newInstanceButton); // Add New Instance button here + buttonSizer.addStretch(); + buttonSizer.add(this.runButton); + buttonSizer.add(this.saveParamsButton); // Add Save Parameters button here + buttonSizer.add(this.resetButton); + buttonSizer.add(this.cancelButton); + + this.sizer.addSpacing(10); + this.sizer.add(buttonSizer); + + // === Platform Toggle === +this.updatePlatformVisibility = function () { + var isTelegram = self.telegramRadio.checked; + var isIMsg = self.imessageRadio.checked; + var isPushover = self.pushoverRadio.checked; + + self.telegramGroupBox.enabled = isTelegram; + self.imessageGroupBox.enabled = isIMsg; + self.pushoverGroupBox.enabled = isPushover; +}; + + this.telegramRadio.onCheck = this.updatePlatformVisibility; + this.imessageRadio.onCheck = this.updatePlatformVisibility; + this.pushoverRadio.onCheck = this.updatePlatformVisibility; + this.updatePlatformVisibility(); + + // Function to retrieve all field values and return them as an object +// Function to retrieve all field values and return them as an object +function getFormData() { + // Determine selected platform dynamically + var selectedPlatform = null; + + if (self.telegramRadio.checked) { + selectedPlatform = PLATFORMS.TELEGRAM; + } else if (self.imessageRadio.checked) { + selectedPlatform = PLATFORMS.IMESSAGE; + } else if (self.pushoverRadio.checked) { + selectedPlatform = PLATFORMS.PUSHOVER; + } + return { + platform: selectedPlatform, + telegramBotToken: self.telegramTokenInput.text.trim(), + telegramChatId: self.telegramChatIdInput.value, + message: self.messageInput.text.trim(), + imessageRecipient: self.imessageRecipientInput.text.trim(), + pushoverUser: self.pushoverUserInput.text.trim(), + pushoverToken: self.pushoverTokenInput.text.trim(), + doStretch: self.doStretchCheckbox.checked, + linked: self.linkedCheckbox.checked + }; +} + + // === Wire Buttons === + // === "New Instance" Button (Triangle Icon) === + var self = this; + this.newInstanceButton.onMousePress = function () { + var P = new Object(); + + // Save all parameters into P + var data = getFormData(); // Retrieve all data from the form + + // Store parameters in the instance + Parameters.clear(); + for (var key in data) { + if (data[key] !== undefined) { + Parameters.set(key, data[key]); + } + } + self.newInstance(); + }; + + // In the run button event, when the user selects iMessage, we use the function +this.runButton.onClick = function () { + // Retrieve form data + var data = getFormData(); + + // Call sendImage with the retrieved form data +sendImage( + ImageWindow.activeWindow, + data.platform, + data.telegramBotToken, + data.telegramChatId, + data.imessageRecipient, + data.pushoverUser, + data.pushoverToken, + data.message, + data.doStretch, + data.linked +); + + self.ok(); +}; + + this.cancelButton.onClick = function () { + self.cancel(); + }; + +// === Save Parameters Button === +this.saveParamsButton.onClick = function () { + // Get all field values from the form + var data = getFormData(); // Use the getFormData function to retrieve the values + + // Save the parameters + savePhoneSettings(data); + + // Notify user that parameters have been saved + Console.noteln("Parameters have been saved successfully."); + + // Show a confirmation dialog +raiseInfo("Parameters saved successfully!"); +}; + +// === Reset Parameters Button === +this.resetButton.onClick = function () { + // Reset all fields to default values + self.telegramTokenInput.text = ''; + self.telegramChatIdInput.setValue(0); + self.imessageRecipientInput.text = ''; + self.messageInput.text = ':)'; + self.pushoverUserInput.text = ''; + self.pushoverTokenInput.text = ''; + self.doStretchCheckbox.checked = false; + self.linkedCheckbox.checked = false; + + // Reset platform selection to default (Telegram) + self.telegramRadio.checked = true; + self.imessageRadio.checked = false; + self.pushoverRadio.checked = false; + + // Reset settings in the memory + resetPhoneSettings(); // Reset stored settings + + Console.noteln("Settings have been reset to default."); +}; + + this.adjustToContents(); +} + +SendMessageToPhoneDialog.prototype = new Dialog(); + +// ==== Main Program ==== +function main() { + // printTheSpaceKoalaBanner(); + + // If the process was dragged onto an image, execute immediately without opening the GUI + if (Parameters.isViewTarget) { + let targetWindow = Parameters.targetView.window; + + if (!targetWindow.isNull) { + // Retrieve parameters from Parameters storage + var platform = parseInt(Parameters.get('platform'), 10); // Telegram or iMessage + var token = Parameters.get('telegramBotToken'); // Telegram Bot Token + var telegramChatId = parseInt(Parameters.get('telegramChatId'), 10); // Telegram Chat ID + var message = Parameters.get('message'); // Message Text + var imessageRecipient = Parameters.get('imessageRecipient'); // iMessage Recipient + var pushoverUser = Parameters.get('pushoverUser'); // Pushover User Key + var pushoverToken = Parameters.get('pushoverToken'); // Pushover App Token + var doStretch = (Parameters.get('doStretch') || "").trim().toLowerCase() === "true"; + var linked = (Parameters.get('linked') || "").trim().toLowerCase() === "true"; + // Send the image depending on the platform + sendImage( + targetWindow, + platform, + token, + telegramChatId, + imessageRecipient, + pushoverUser, + pushoverToken, + message, + doStretch, // Include the doStretch parameter + linked // Include the linked parameter + ); + } else { + printError('No valid target image found.'); + } + return; // Exit after processing the target image + } + + // Otherwise: Open the GUI for user input + let dialog = new SendMessageToPhoneDialog(); + + if (!dialog.execute()) { + printInfo('User canceled the script.'); + return; + } +} + +main(); diff --git a/pixinsight_update_bundle/src/scripts/TheSpaceKoala/SendMessageToPhone.xsgn b/pixinsight_update_bundle/src/scripts/TheSpaceKoala/SendMessageToPhone.xsgn new file mode 100644 index 0000000..a5a78d1 --- /dev/null +++ b/pixinsight_update_bundle/src/scripts/TheSpaceKoala/SendMessageToPhone.xsgn @@ -0,0 +1,12 @@ + + + + 2025-06-04T17:39:49.391Z + + 2025-06-04T17:39:49.390Z + 3ejjWsbNolVimxh5rJ6Maf6PCnH6AKo+A+ZfM1I64LtlcxVEBAhxZTkFsvZIjWL4ybTba9FSZeIrlBTDOFjrCg== + + diff --git a/pixinsight_update_bundle/src/scripts/TheSpaceKoala/SendMessageToPushover.js b/pixinsight_update_bundle/src/scripts/TheSpaceKoala/SendMessageToPushover.js new file mode 100644 index 0000000..e2c721c --- /dev/null +++ b/pixinsight_update_bundle/src/scripts/TheSpaceKoala/SendMessageToPushover.js @@ -0,0 +1,100 @@ +// ================================================================================== +// SendMessageToPushover.js +// +// Copyright (c) 2025 Luca Bartek, Brian Weber +// Send Message to Pushover +// Version: V1.0.1 //20250521 +// Author: Luca Bartek, Brian Weber +// Website: thespacekoala.com, briangweber.com +// +// Redistribution and use in both source and binary forms, with or without +// modification, is permitted provided that the following conditions are met: +// +// 1. All redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// +// 2. All redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// ================================================================================== + + +// Function to send a Pushover message +function sendMessageToPushover(userKey, appToken, message) { + var url = "https://api.pushover.net/1/messages.json"; + var args = [ + "-s", "-X", "POST", + url, + "-d", "token=" + appToken, + "-d", "user=" + userKey, + "-d", "message=" + message + ]; + + var p = new ExternalProcess(); + ExternalProcess.execute('curl', args); +} + + +function sendImageToPushover(imagePath, userKey, appToken, caption) { + var url = "https://api.pushover.net/1/messages.json"; + var args = [ + "-s", "-X", "POST", + url, + "-F", "token=" + appToken, + "-F", "user=" + userKey, + "-F", "message=" + caption, + "-F", "attachment=@" + imagePath + ]; + console.noteln('imagePath '+imagePath); + + // Execute curl command + let exitCode = ExternalProcess.execute('curl', args); + + // Check if the request was successful + if (exitCode !== 0) { + printError('Failed to send photo. Curl exit code: ' + exitCode); + } else { + printInfo('Photo successfully sent to Pushover!'); + } +} + +function processAndSendImageToPushover( + imageWindow, + userKey, + appToken, + message, + doStretch, + linked +) { + // Duplicate the image window + var duplicateWindow = duplicateImageWindow(imageWindow, 'PushoverProcessed'); + + // Apply auto-stretch if the flag is set + if (doStretch == true) { + applyAutoStretch(duplicateWindow.mainView, linked); + } + + var width = duplicateWindow.mainView.image.width; + var height = duplicateWindow.mainView.image.height; + var resampleFactor = 1; + + // Loop to resample the image if necessary + while (width / resampleFactor + height / resampleFactor > 10000) { + resampleFactor++; + } + + // Downsample if necessary + if (resampleFactor > 1) { + downsampleImage(duplicateWindow.mainView, -resampleFactor); + } + + // Save the image as JPEG + var jpegPath = saveImageAsJpeg(duplicateWindow, null, 'PushoverProcessed'); + console.noteln('JPEG saved at: ' + jpegPath); + + // Send the image + sendImageToPushover(jpegPath, userKey, appToken, message); + + // Close the duplicate window after processing + duplicateWindow.forceClose(); +} diff --git a/pixinsight_update_bundle/src/scripts/TheSpaceKoala/SendMessageToTelegram.js b/pixinsight_update_bundle/src/scripts/TheSpaceKoala/SendMessageToTelegram.js new file mode 100644 index 0000000..4fb9bfa --- /dev/null +++ b/pixinsight_update_bundle/src/scripts/TheSpaceKoala/SendMessageToTelegram.js @@ -0,0 +1,108 @@ +// ================================================================================== +// SendMessageToTelegram.js +// +// Copyright (c) 2025 Luca Bartek +// Notification Event Script +// Version: V1.0.1 //20250521 +// Author: Luca Bartek +// Website: www.thespacekoala.com +// +// Redistribution and use in both source and binary forms, with or without +// modification, is permitted provided that the following conditions are met: +// +// 1. All redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// +// 2. All redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// ================================================================================== + +//#include "KoalaUtils.js" + + +// Function to send a Telegram message +function sendMessageToTelegram(botToken, chatId, message) { + var url = "https://api.telegram.org/bot" + botToken + "/sendMessage"; + var args = [ + "-s", "-X", "POST", + url, + "-d", "chat_id=" + chatId, + "-d", "text=" + message + ]; + + var p = new ExternalProcess(); + ExternalProcess.execute('curl', args); +} + + +function sendImageToTelegram(imagePath, telegramToken, chatId, message) { + const url = 'https://api.telegram.org/bot' + telegramToken + '/sendPhoto'; + let args = [ + '-s', + '-X', + 'POST', + url, + '-F', + 'chat_id=' + chatId, + '-F', + 'photo=@' + imagePath, + ]; + + if (message) { + args[args.length] = '-F'; + args[args.length] = 'caption=' + message; + } + + // Execute curl command + let exitCode = ExternalProcess.execute('curl', args); + + // Check if the request was successful + if (exitCode !== 0) { + printError('Failed to send photo. Curl exit code: ' + exitCode); + } else { + printInfo('Photo successfully sent to Telegram!'); + } +} + +function processAndSendImageToTelegram( + imageWindow, + telegramToken, + telegramChatId, + message, + doStretch, + linked +) { + // Duplicate the image window + var duplicateWindow = duplicateImageWindow(imageWindow, 'TelegramProcessed'); + + // Apply auto-stretch if the flag is set + if (doStretch == true) { + applyAutoStretch(duplicateWindow.mainView, linked); + } + + var width = duplicateWindow.mainView.image.width; + var height = duplicateWindow.mainView.image.height; + var resampleFactor = 1; + + // Loop to resample the image if necessary + while (width / resampleFactor + height / resampleFactor > 10000) { + resampleFactor++; + } + + // Downsample if necessary + if (resampleFactor > 1) { + downsampleImage(duplicateWindow.mainView, -resampleFactor); + } + + // Save the image as JPEG + var jpegPath = saveImageAsJpeg(duplicateWindow, null, 'TelegramProcessed'); + console.noteln('JPEG saved at: ' + jpegPath); + + // Send the image via Telegram + sendImageToTelegram(jpegPath, telegramToken, telegramChatId, message); + + // Close the duplicate window after processing + duplicateWindow.forceClose(); +} diff --git a/pixinsight_update_bundle/src/scripts/TheSpaceKoala/messageIcon.svg b/pixinsight_update_bundle/src/scripts/TheSpaceKoala/messageIcon.svg new file mode 100644 index 0000000..5fe4d83 --- /dev/null +++ b/pixinsight_update_bundle/src/scripts/TheSpaceKoala/messageIcon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/pixinsight_update_bundle/src/scripts/TheSpaceKoala/telegram.svg b/pixinsight_update_bundle/src/scripts/TheSpaceKoala/telegram.svg new file mode 100644 index 0000000..5b2ed60 --- /dev/null +++ b/pixinsight_update_bundle/src/scripts/TheSpaceKoala/telegram.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + +