Compare commits

...
190 Commits
Author SHA1 Message Date
Anthony Stirling 3ffe489e32 test 2026-01-05 15:00:37 +00:00
Anthony Stirling acbddf88d2 tests 2026-01-04 21:17:35 +00:00
Anthony Stirling 23ac6392bb todo doc example 2025-12-30 18:21:13 +00:00
Anthony Stirling d2fd454d6e ai random 2025-12-27 23:46:47 +00:00
182eb504de [V2] feat(convert): add support for CBR to PDF and PDF to CBR conversions (#4833)
# Description of Changes
TLDR:
- Introduced `ConvertFromCbrSettings` and `ConvertToCbrSettings`
components for handling conversion-specific settings.
- Updated `ConvertSettings` to include CBR-related configurations.
- Updated `useConvertParameters` with new parameters: `cbrOptions` and
`pdfToCbrOptions`.
- Updated locales with CBR-related translations.
- Added endpoints and updated conversion matrix for CBR formats in
`convertConstants`.
- Modified `useConvertOperation` to handle CBR-specific parameters
during form data preparation.


For backend see this PR: #4581

This pull request adds support for converting between CBR (Comic Book
Archive) and PDF formats in the frontend. It introduces new UI
components for configuring conversion options, updates the conversion
constants and logic, and ensures parameters for these conversions are
handled correctly throughout the app.

**CBR/PDF Conversion Features:**

* Added new UI components: `ConvertFromCbrSettings` for CBR to PDF
conversions (option to optimize for ebook readers), and
`ConvertToCbrSettings` for PDF to CBR conversions (DPI selection for
image rendering). These are conditionally rendered in
`ConvertSettings.tsx` based on selected formats.

**Conversion Logic and Constants:**

* Extended conversion endpoints, format options, conversion matrix, and
endpoint mappings to support CBR to PDF and PDF to CBR conversions.
* Updated `shouldProcessFilesSeparately` logic to handle PDF to CBR
conversions, ensuring each PDF generates its own archive.

**Parameter Handling:**

* Added new parameters (`cbrOptions` and `pdfToCbrOptions`) to the
conversion parameter interface and default values, ensuring they are
set/reset appropriately during format changes
* Modified form data construction to include new options for CBR/PDF
conversions (optimize for ebook, DPI).

### Frontend
<img width="1291" height="861" alt="image"
src="https://github.com/user-attachments/assets/fb63be66-6f40-4dde-8235-86c9ddfa1f7c"
/>


<img width="1411" height="1000" alt="image"
src="https://github.com/user-attachments/assets/529593c4-6f32-4b11-9754-7f334f40d32e"
/>


Note on RAR5. You can go back-and-forth with this converter due to the
fact on default pdf to cbr makes RAR5 meanwhile, cbr to pdf can only
RAR4 and below. This is unfortunate limitation of JunRAR library. In the
real world, generally RAR5s are not that common, and they mostly
unsupported by open-source software.

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [X] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [X] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [X] I have performed a self-review of my own code
- [X] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [X] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [X] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Signed-off-by: Balázs Szücs <bszucs1209@gmail.com>
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
Co-authored-by: Reece Browne <74901996+reecebrowne@users.noreply.github.com>
2025-12-27 01:32:55 +00:00
Balázs Szücs 98fa5dfcc1 [V2] feat(sign): add automatic white background removal for signature images (#5210)
# Description of Changes



This pull request adds a new feature that allows users to automatically
remove the white background from uploaded signature images, making them
transparent. The changes include UI improvements for image uploading,
new image processing utilities, and updates to translation files for
better user feedback.

### Image Upload & Processing Enhancements

* Added a `removeWhiteBackground` utility function in
`imageTransparency.ts` to process images and remove white backgrounds,
with options for automatic corner color detection and tolerance
settings.
* Updated the `ImageUploader` component to support background removal,
including a checkbox for toggling the feature, processing state
feedback, and integration with the new utility

### Signature Tool Integration

* Modified `SignSettings.tsx` to enable the background removal feature
for signature images, handle processed image data, and ensure signature
placement logic works seamlessly with the new feature.

### UI & Localization Updates

* Added new translation strings for background removal and image
processing feedback in the English locale file.



https://github.com/user-attachments/assets/28263940-1756-4f0e-9bfb-5603a6fb8a2c


<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [X] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [X] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [X] I have performed a self-review of my own code
- [X] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [X] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Signed-off-by: Balázs Szücs <bszucs1209@gmail.com>
2025-12-26 23:50:21 +00:00
albanobattistella 429520d3f9 Update translation.toml (#5308)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [x] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [x] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [x] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [x] I have performed a self-review of my own code
- [x] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-12-26 22:37:59 +00:00
stirlingbot[bot] fbfe1da7da 🤖 format everything with pre-commit by stirlingbot (#5306)
Auto-generated by [create-pull-request][1] with **stirlingbot**

[1]: https://github.com/peter-evans/create-pull-request

Signed-off-by: stirlingbot[bot] <stirlingbot[bot]@users.noreply.github.com>
Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com>
2025-12-25 23:39:05 +00:00
Ludy c047bdda8c feat(translations): add command to remove unused translation keys (#5292)
# Description of Changes

This pull request adds a new feature to the translation management
script, allowing users to automatically remove unused translations that
are not present in the `en-GB` (golden truth) file. The update includes
both the implementation in `translation_merger.py` and documentation in
`README.md`. The most important changes are grouped below:

**New Feature: Remove Unused Translations**

* Added a `remove-unused` command to `translation_merger.py`, which
removes translation keys from a target file if they are not present in
the `en-GB` reference. This includes backup support and a CLI interface.
[[1]](diffhunk://#diff-bb7f09f590ccabb456565d52afd66c1421525ddb1b6b65c0985aaab5a3c20ae7R282-R307)
[[2]](diffhunk://#diff-bb7f09f590ccabb456565d52afd66c1421525ddb1b6b65c0985aaab5a3c20ae7R397-R404)
[[3]](diffhunk://#diff-bb7f09f590ccabb456565d52afd66c1421525ddb1b6b65c0985aaab5a3c20ae7R464-R471)
* Implemented helper methods `_delete_nested_key` (for deleting nested
keys and cleaning up empty branches) and `get_unused_keys` (for
identifying unused keys) in the translation merger logic.
[[1]](diffhunk://#diff-bb7f09f590ccabb456565d52afd66c1421525ddb1b6b65c0985aaab5a3c20ae7R117-R139)
[[2]](diffhunk://#diff-bb7f09f590ccabb456565d52afd66c1421525ddb1b6b65c0985aaab5a3c20ae7R156-R166)

**Documentation Updates**

* Updated `README.md` to document the new `remove-unused` command and
describe its purpose and usage.
[[1]](diffhunk://#diff-aeee54608ec45b06f4d52a4afcfbc7e6b08dee75f4328207cec32b8ca02fe0c6R185-R187)
[[2]](diffhunk://#diff-aeee54608ec45b06f4d52a4afcfbc7e6b08dee75f4328207cec32b8ca02fe0c6R196)

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-12-25 23:24:30 +00:00
Balázs Szücs a33e22559b [V2] feat(convert): add eBook formats (EPUB, MOBI, AZW3, FB2) to supported formats list (#5307)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

Signed-off-by: Balázs Szücs <bszucs1209@gmail.com>
2025-12-25 20:02:58 +00:00
Balázs Szücs 1318604f32 [V2] feat(convert): add eBook (EPUB, MOBI, AZW3, FB2) to PDF conversion options and UI (#5291)
# Description of Changes

This pull request adds support for converting eBook formats (EPUB, MOBI,
AZW3, FB2) to PDF, including a new set of user-configurable options for
the conversion process. It introduces frontend UI components, updates
configuration and constants, and ensures that the backend can recognize
and handle these new formats and options.

**eBook to PDF Conversion Support**

- Added support for converting eBook formats (`epub`, `mobi`, `azw3`,
`fb2`) to PDF, including updates to conversion endpoints, extension
mappings, and conversion matrices in `convertConstants.ts` to route
these conversions through the new `ebook-to-pdf` endpoint.
- Updated the file processing logic so that eBook-to-PDF conversions are
processed separately for each file, similar to web file conversions.

**Frontend: User Options for eBook Conversion**

- Added a new UI component `ConvertFromEbookSettings.tsx` that allows
users to configure options for eBook-to-PDF conversion: embedding all
fonts, including a table of contents, adding page numbers, and
optimizing for eBook readers. This component is conditionally rendered
in the conversion settings when an eBook format is selected as the
source and PDF as the target.
- Integrated the new eBook options into the conversion parameters and
ensured they are initialized/reset appropriately in the conversion
settings.
**Form Data and Backend Integration**

- Modified the form data builder to append the new eBook options to the
request payload when performing eBook-to-PDF conversions, ensuring these
settings are sent to the backend.

**Localization and Configuration**

- Added localization strings for all new eBook conversion options in the
English translation file, providing user-friendly labels and
descriptions in the UI.
- Updated backend configuration logic to recognize `Calibre` and
`FFmpeg` as tool groups, ensuring correct enablement/disablement
behavior for these tools.

<img width="366" height="997" alt="image"
src="https://github.com/user-attachments/assets/44d3308c-ea49-4874-8f5e-c7d617a37489"
/>

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [X] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [X] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [X] I have performed a self-review of my own code
- [X] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [X] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [X] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Signed-off-by: Balázs Szücs <bszucs1209@gmail.com>
2025-12-25 14:58:10 +00:00
Balázs Szücs 43a5f72b01 [V2] feat(crop): add auto-crop whitespace option to crop tool UI (#5275)
# Description of Changes

This pull request adds an auto-crop feature to the PDF crop tool,
allowing users to automatically crop whitespace from PDFs. The UI now
includes an "Auto-crop whitespace" checkbox, and when enabled, manual
crop controls are hidden. The crop operation logic and form data
submission have been updated to support this new option.

**Auto-crop Feature Implementation**

* Added an `autoCrop` boolean parameter to the `CropParameters`
interface and set its default value to `false` in
`useCropParameters.ts`.
* Updated the crop operation logic in `useCropOperation.ts` to include
the `autoCrop` parameter in the form data and only send manual crop
coordinates if `autoCrop` is disabled.

**User Interface Updates**

* Added an "Auto-crop whitespace" checkbox to the crop settings UI in
`CropSettings.tsx`, which toggles the auto-crop feature
* Modified the crop settings UI to hide manual crop controls and
validation alerts when auto-crop is enabled

**Localization**

* Added a new translation string for "Auto-crop whitespace" in the
English locale file `translation.toml`.



<img width="363" height="998" alt="image"
src="https://github.com/user-attachments/assets/a92988b8-eea0-47e7-961f-b4a6e018ff2f"
/>

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [X] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [X] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [X] I have performed a self-review of my own code
- [X] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [X] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [X] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

Signed-off-by: Balázs Szücs <bszucs1209@gmail.com>
2025-12-25 14:22:46 +00:00
dependabot[bot] d97820c232 build(deps): bump docker/metadata-action from 5.8.0 to 5.10.0 (#5299)
Bumps
[docker/metadata-action](https://github.com/docker/metadata-action) from
5.8.0 to 5.10.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/docker/metadata-action/releases">docker/metadata-action's
releases</a>.</em></p>
<blockquote>
<h2>v5.10.0</h2>
<ul>
<li>Bump <code>@​docker/actions-toolkit</code> from 0.66.0 to 0.68.0 in
<a
href="https://redirect.github.com/docker/metadata-action/pull/559">docker/metadata-action#559</a>
<a
href="https://redirect.github.com/docker/metadata-action/pull/569">docker/metadata-action#569</a></li>
<li>Bump js-yaml from 3.14.1 to 3.14.2 in <a
href="https://redirect.github.com/docker/metadata-action/pull/564">docker/metadata-action#564</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/docker/metadata-action/compare/v5.9.0...v5.10.0">https://github.com/docker/metadata-action/compare/v5.9.0...v5.10.0</a></p>
<h2>v5.9.0</h2>
<ul>
<li>Add <code>tag-names</code> output to return tag names without image
base name by <a
href="https://github.com/crazy-max"><code>@​crazy-max</code></a> in <a
href="https://redirect.github.com/docker/metadata-action/pull/553">docker/metadata-action#553</a></li>
<li>Bump <code>@​babel/runtime-corejs3</code> from 7.14.7 to 7.28.2 in
<a
href="https://redirect.github.com/docker/metadata-action/pull/539">docker/metadata-action#539</a></li>
<li>Bump <code>@​docker/actions-toolkit</code> from 0.62.1 to 0.66.0 in
<a
href="https://redirect.github.com/docker/metadata-action/pull/555">docker/metadata-action#555</a></li>
<li>Bump brace-expansion from 1.1.11 to 1.1.12 in <a
href="https://redirect.github.com/docker/metadata-action/pull/540">docker/metadata-action#540</a></li>
<li>Bump csv-parse from 5.6.0 to 6.1.0 in <a
href="https://redirect.github.com/docker/metadata-action/pull/532">docker/metadata-action#532</a></li>
<li>Bump semver from 7.7.2 to 7.7.3 in in <a
href="https://redirect.github.com/docker/metadata-action/pull/554">docker/metadata-action#554</a></li>
<li>Bump tmp from 0.2.3 to 0.2.5 in <a
href="https://redirect.github.com/docker/metadata-action/pull/541">docker/metadata-action#541</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/docker/metadata-action/compare/v5.8.0...v5.9.0">https://github.com/docker/metadata-action/compare/v5.8.0...v5.9.0</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/docker/metadata-action/commit/c299e40c65443455700f0fdfc63efafe5b349051"><code>c299e40</code></a>
Merge pull request <a
href="https://redirect.github.com/docker/metadata-action/issues/569">#569</a>
from docker/dependabot/npm_and_yarn/docker/actions-to...</li>
<li><a
href="https://github.com/docker/metadata-action/commit/f015d7914a06d5c4931affc0780479c2336fd8e3"><code>f015d79</code></a>
chore: update generated content</li>
<li><a
href="https://github.com/docker/metadata-action/commit/121bcc2ca8f5f246e9af338aeb41e55825fe7c88"><code>121bcc2</code></a>
chore(deps): Bump <code>@​docker/actions-toolkit</code> from 0.67.0 to
0.68.0</li>
<li><a
href="https://github.com/docker/metadata-action/commit/f7b6bf41b94feca9834c527e3bd584efa2e7280b"><code>f7b6bf4</code></a>
Merge pull request <a
href="https://redirect.github.com/docker/metadata-action/issues/564">#564</a>
from docker/dependabot/npm_and_yarn/js-yaml-3.14.2</li>
<li><a
href="https://github.com/docker/metadata-action/commit/0b95c6b8604d853d90ab386caf3a1e754d9697f7"><code>0b95c6b</code></a>
Merge pull request <a
href="https://redirect.github.com/docker/metadata-action/issues/565">#565</a>
from docker/dependabot/github_actions/actions/checkout-6</li>
<li><a
href="https://github.com/docker/metadata-action/commit/17f70d7525d8de2c783e41c092e28219c8fc2a67"><code>17f70d7</code></a>
Merge pull request <a
href="https://redirect.github.com/docker/metadata-action/issues/568">#568</a>
from motoki317/docs/fix-to-24h-schedule-pattern</li>
<li><a
href="https://github.com/docker/metadata-action/commit/afd7e6d7bbf70ea7bc2e4f3c782fe1feabe42d88"><code>afd7e6d</code></a>
docs(README): Fix date format from 12h to 24h in schedule pattern</li>
<li><a
href="https://github.com/docker/metadata-action/commit/602aff8e11accbaf5f2233069201ffe332de3d5e"><code>602aff8</code></a>
chore(deps): Bump actions/checkout from 5 to 6</li>
<li><a
href="https://github.com/docker/metadata-action/commit/aecb1a49a52523dc3b4dcab352cae7572eb61c87"><code>aecb1a4</code></a>
chore(deps): Bump js-yaml from 3.14.1 to 3.14.2</li>
<li><a
href="https://github.com/docker/metadata-action/commit/8d8c7c12f7b958582a5cb82ba16d5903cb27976a"><code>8d8c7c1</code></a>
Merge pull request <a
href="https://redirect.github.com/docker/metadata-action/issues/559">#559</a>
from docker/dependabot/npm_and_yarn/docker/actions-to...</li>
<li>Additional commits viewable in <a
href="https://github.com/docker/metadata-action/compare/v5.8.0...c299e40c65443455700f0fdfc63efafe5b349051">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=docker/metadata-action&package-manager=github_actions&previous-version=5.8.0&new-version=5.10.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-25 13:49:29 +00:00
dependabot[bot]andAnthony Stirling 03cd7c1456 build(deps): bump peter-evans/create-pull-request from 7.0.8 to 8.0.0 (#5300)
Bumps
[peter-evans/create-pull-request](https://github.com/peter-evans/create-pull-request)
from 7.0.8 to 8.0.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/peter-evans/create-pull-request/releases">peter-evans/create-pull-request's
releases</a>.</em></p>
<blockquote>
<h2>Create Pull Request v8.0.0</h2>
<h2>What's new in v8</h2>
<ul>
<li>Requires <a
href="https://github.com/actions/runner/releases/tag/v2.327.1">Actions
Runner v2.327.1</a> or later if you are using a self-hosted runner for
Node 24 support.</li>
</ul>
<h2>What's Changed</h2>
<ul>
<li>chore: Update checkout action version to v6 by <a
href="https://github.com/yonas"><code>@​yonas</code></a> in <a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/4258">peter-evans/create-pull-request#4258</a></li>
<li>Update actions/checkout references to <a
href="https://github.com/v6"><code>@​v6</code></a> in docs by <a
href="https://github.com/Copilot"><code>@​Copilot</code></a> in <a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/4259">peter-evans/create-pull-request#4259</a></li>
<li>feat: v8 by <a
href="https://github.com/peter-evans"><code>@​peter-evans</code></a> in
<a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/4260">peter-evans/create-pull-request#4260</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/yonas"><code>@​yonas</code></a> made
their first contribution in <a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/4258">peter-evans/create-pull-request#4258</a></li>
<li><a href="https://github.com/Copilot"><code>@​Copilot</code></a> made
their first contribution in <a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/4259">peter-evans/create-pull-request#4259</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/peter-evans/create-pull-request/compare/v7.0.11...v8.0.0">https://github.com/peter-evans/create-pull-request/compare/v7.0.11...v8.0.0</a></p>
<h2>Create Pull Request v7.0.11</h2>
<h2>What's Changed</h2>
<ul>
<li>fix: restrict remote prune to self-hosted runners by <a
href="https://github.com/peter-evans"><code>@​peter-evans</code></a> in
<a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/4250">peter-evans/create-pull-request#4250</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/peter-evans/create-pull-request/compare/v7.0.10...v7.0.11">https://github.com/peter-evans/create-pull-request/compare/v7.0.10...v7.0.11</a></p>
<h2>Create Pull Request v7.0.10</h2>
<p>⚙️ Fixes an issue where updating a pull request failed when targeting
a forked repository with the same owner as its parent.</p>
<h2>What's Changed</h2>
<ul>
<li>build(deps): bump the github-actions group with 2 updates by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/4235">peter-evans/create-pull-request#4235</a></li>
<li>build(deps-dev): bump prettier from 3.6.2 to 3.7.3 in the npm group
by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/4240">peter-evans/create-pull-request#4240</a></li>
<li>fix: provider list pulls fallback for multi fork same owner by <a
href="https://github.com/peter-evans"><code>@​peter-evans</code></a> in
<a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/4245">peter-evans/create-pull-request#4245</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/obnyis"><code>@​obnyis</code></a> made
their first contribution in <a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/4064">peter-evans/create-pull-request#4064</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/peter-evans/create-pull-request/compare/v7.0.9...v7.0.10">https://github.com/peter-evans/create-pull-request/compare/v7.0.9...v7.0.10</a></p>
<h2>Create Pull Request v7.0.9</h2>
<p>⚙️ Fixes an <a
href="https://redirect.github.com/peter-evans/create-pull-request/issues/4228">incompatibility</a>
with the recently released <code>actions/checkout@v6</code>.</p>
<h2>What's Changed</h2>
<ul>
<li>~70 dependency updates by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a></li>
<li>docs: fix workaround description about <code>ready_for_review</code>
by <a href="https://github.com/ybiquitous"><code>@​ybiquitous</code></a>
in <a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/3939">peter-evans/create-pull-request#3939</a></li>
<li>Docs: <code>add-paths</code> default behavior by <a
href="https://github.com/joeflack4"><code>@​joeflack4</code></a> in <a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/3928">peter-evans/create-pull-request#3928</a></li>
<li>docs: update to create-github-app-token v2 by <a
href="https://github.com/Goooler"><code>@​Goooler</code></a> in <a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/4063">peter-evans/create-pull-request#4063</a></li>
<li>Fix compatibility with actions/checkout@v6 by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/4230">peter-evans/create-pull-request#4230</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/joeflack4"><code>@​joeflack4</code></a>
made their first contribution in <a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/3928">peter-evans/create-pull-request#3928</a></li>
<li><a href="https://github.com/Goooler"><code>@​Goooler</code></a> made
their first contribution in <a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/4063">peter-evans/create-pull-request#4063</a></li>
<li><a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> made
their first contribution in <a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/4230">peter-evans/create-pull-request#4230</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/peter-evans/create-pull-request/commit/98357b18bf14b5342f975ff684046ec3b2a07725"><code>98357b1</code></a>
feat: v8 (<a
href="https://redirect.github.com/peter-evans/create-pull-request/issues/4260">#4260</a>)</li>
<li><a
href="https://github.com/peter-evans/create-pull-request/commit/41c0e4b7899a4a0922bf899d64c5f25738cfe356"><code>41c0e4b</code></a>
Update actions/checkout references to <a
href="https://github.com/v6"><code>@​v6</code></a> in docs (<a
href="https://redirect.github.com/peter-evans/create-pull-request/issues/4259">#4259</a>)</li>
<li><a
href="https://github.com/peter-evans/create-pull-request/commit/994332de4c8124517167807167073cf397678768"><code>994332d</code></a>
chore: Update checkout action version to v6 (<a
href="https://redirect.github.com/peter-evans/create-pull-request/issues/4258">#4258</a>)</li>
<li><a
href="https://github.com/peter-evans/create-pull-request/commit/22a9089034f40e5a961c8808d113e2c98fb63676"><code>22a9089</code></a>
fix: restrict remote prune to self-hosted runners (<a
href="https://redirect.github.com/peter-evans/create-pull-request/issues/4250">#4250</a>)</li>
<li><a
href="https://github.com/peter-evans/create-pull-request/commit/d4f3be6ce6f4083b7ac7490ab98b48a62db1ee41"><code>d4f3be6</code></a>
fix: provider list pulls fallback for multi fork same owner (<a
href="https://redirect.github.com/peter-evans/create-pull-request/issues/4245">#4245</a>)</li>
<li><a
href="https://github.com/peter-evans/create-pull-request/commit/bc8a47f5657f110049f4afd030c95529a9c62b76"><code>bc8a47f</code></a>
build(deps-dev): bump prettier from 3.6.2 to 3.7.3 in the npm group (<a
href="https://redirect.github.com/peter-evans/create-pull-request/issues/4240">#4240</a>)</li>
<li><a
href="https://github.com/peter-evans/create-pull-request/commit/a67ef28ca5df73d51a15007068e5931257943b0d"><code>a67ef28</code></a>
build(deps): bump the github-actions group with 2 updates (<a
href="https://redirect.github.com/peter-evans/create-pull-request/issues/4235">#4235</a>)</li>
<li><a
href="https://github.com/peter-evans/create-pull-request/commit/84ae59a2cdc2258d6fa0732dd66352dddae2a412"><code>84ae59a</code></a>
fix: compatibility with actions/checkout@v6 (<a
href="https://redirect.github.com/peter-evans/create-pull-request/issues/4230">#4230</a>)</li>
<li><a
href="https://github.com/peter-evans/create-pull-request/commit/b4733b9419fd47bbfa1807b15627e17cd70b5b22"><code>b4733b9</code></a>
build(deps-dev): bump js-yaml from 4.1.0 to 4.1.1 (<a
href="https://redirect.github.com/peter-evans/create-pull-request/issues/4222">#4222</a>)</li>
<li><a
href="https://github.com/peter-evans/create-pull-request/commit/0edc001d28a2959cd7a6b505629f1d82f0a6e67d"><code>0edc001</code></a>
build(deps-dev): bump the npm group with 2 updates (<a
href="https://redirect.github.com/peter-evans/create-pull-request/issues/4201">#4201</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/peter-evans/create-pull-request/compare/v7.0.8...98357b18bf14b5342f975ff684046ec3b2a07725">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=peter-evans/create-pull-request&package-manager=github_actions&previous-version=7.0.8&new-version=8.0.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2025-12-25 13:40:33 +00:00
dependabot[bot] b1b37366e8 build(deps): bump actions/github-script from 7.0.1 to 8.0.0 (#5302)
Bumps [actions/github-script](https://github.com/actions/github-script)
from 7.0.1 to 8.0.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/github-script/releases">actions/github-script's
releases</a>.</em></p>
<blockquote>
<h2>v8.0.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Update Node.js version support to 24.x by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/637">actions/github-script#637</a></li>
<li>README for updating actions/github-script from v7 to v8 by <a
href="https://github.com/sneha-krip"><code>@​sneha-krip</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/653">actions/github-script#653</a></li>
</ul>
<h2>⚠️ Minimum Compatible Runner Version</h2>
<p><strong>v2.327.1</strong><br />
<a
href="https://github.com/actions/runner/releases/tag/v2.327.1">Release
Notes</a></p>
<p>Make sure your runner is updated to this version or newer to use this
release.</p>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/github-script/pull/637">actions/github-script#637</a></li>
<li><a
href="https://github.com/sneha-krip"><code>@​sneha-krip</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/github-script/pull/653">actions/github-script#653</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/github-script/compare/v7.1.0...v8.0.0">https://github.com/actions/github-script/compare/v7.1.0...v8.0.0</a></p>
<h2>v7.1.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Upgrade husky to v9 by <a
href="https://github.com/benelan"><code>@​benelan</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/482">actions/github-script#482</a></li>
<li>Add workflow file for publishing releases to immutable action
package by <a
href="https://github.com/Jcambass"><code>@​Jcambass</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/485">actions/github-script#485</a></li>
<li>Upgrade IA Publish by <a
href="https://github.com/Jcambass"><code>@​Jcambass</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/486">actions/github-script#486</a></li>
<li>Fix workflow status badges by <a
href="https://github.com/joshmgross"><code>@​joshmgross</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/497">actions/github-script#497</a></li>
<li>Update usage of <code>actions/upload-artifact</code> by <a
href="https://github.com/joshmgross"><code>@​joshmgross</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/512">actions/github-script#512</a></li>
<li>Clear up package name confusion by <a
href="https://github.com/joshmgross"><code>@​joshmgross</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/514">actions/github-script#514</a></li>
<li>Update dependencies with <code>npm audit fix</code> by <a
href="https://github.com/joshmgross"><code>@​joshmgross</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/515">actions/github-script#515</a></li>
<li>Specify that the used script is JavaScript by <a
href="https://github.com/timotk"><code>@​timotk</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/478">actions/github-script#478</a></li>
<li>chore: Add Dependabot for NPM and Actions by <a
href="https://github.com/nschonni"><code>@​nschonni</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/472">actions/github-script#472</a></li>
<li>Define <code>permissions</code> in workflows and update actions by
<a href="https://github.com/joshmgross"><code>@​joshmgross</code></a> in
<a
href="https://redirect.github.com/actions/github-script/pull/531">actions/github-script#531</a></li>
<li>chore: Add Dependabot for .github/actions/install-dependencies by <a
href="https://github.com/nschonni"><code>@​nschonni</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/532">actions/github-script#532</a></li>
<li>chore: Remove .vscode settings by <a
href="https://github.com/nschonni"><code>@​nschonni</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/533">actions/github-script#533</a></li>
<li>ci: Use github/setup-licensed by <a
href="https://github.com/nschonni"><code>@​nschonni</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/473">actions/github-script#473</a></li>
<li>make octokit instance available as octokit on top of github, to make
it easier to seamlessly copy examples from GitHub rest api or octokit
documentations by <a
href="https://github.com/iamstarkov"><code>@​iamstarkov</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/508">actions/github-script#508</a></li>
<li>Remove <code>octokit</code> README updates for v7 by <a
href="https://github.com/joshmgross"><code>@​joshmgross</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/557">actions/github-script#557</a></li>
<li>docs: add &quot;exec&quot; usage examples by <a
href="https://github.com/neilime"><code>@​neilime</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/546">actions/github-script#546</a></li>
<li>Bump ruby/setup-ruby from 1.213.0 to 1.222.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/github-script/pull/563">actions/github-script#563</a></li>
<li>Bump ruby/setup-ruby from 1.222.0 to 1.229.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/github-script/pull/575">actions/github-script#575</a></li>
<li>Clearly document passing inputs to the <code>script</code> by <a
href="https://github.com/joshmgross"><code>@​joshmgross</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/603">actions/github-script#603</a></li>
<li>Update README.md by <a
href="https://github.com/nebuk89"><code>@​nebuk89</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/610">actions/github-script#610</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/benelan"><code>@​benelan</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/github-script/pull/482">actions/github-script#482</a></li>
<li><a href="https://github.com/Jcambass"><code>@​Jcambass</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/github-script/pull/485">actions/github-script#485</a></li>
<li><a href="https://github.com/timotk"><code>@​timotk</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/github-script/pull/478">actions/github-script#478</a></li>
<li><a
href="https://github.com/iamstarkov"><code>@​iamstarkov</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/github-script/pull/508">actions/github-script#508</a></li>
<li><a href="https://github.com/neilime"><code>@​neilime</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/github-script/pull/546">actions/github-script#546</a></li>
<li><a href="https://github.com/nebuk89"><code>@​nebuk89</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/github-script/pull/610">actions/github-script#610</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/github-script/compare/v7...v7.1.0">https://github.com/actions/github-script/compare/v7...v7.1.0</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/actions/github-script/commit/ed597411d8f924073f98dfc5c65a23a2325f34cd"><code>ed59741</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/github-script/issues/653">#653</a>
from actions/sneha-krip/readme-for-v8</li>
<li><a
href="https://github.com/actions/github-script/commit/2dc352e4baefd91bec0d06f6ae2f1045d1687ca3"><code>2dc352e</code></a>
Bold minimum Actions Runner version in README</li>
<li><a
href="https://github.com/actions/github-script/commit/01e118c8d0d22115597e46514b5794e7bc3d56f1"><code>01e118c</code></a>
Update README for Node 24 runtime requirements</li>
<li><a
href="https://github.com/actions/github-script/commit/8b222ac82eda86dcad7795c9d49b839f7bf5b18b"><code>8b222ac</code></a>
Apply suggestion from <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a></li>
<li><a
href="https://github.com/actions/github-script/commit/adc0eeac992408a7b276994ca87edde1c8ce4d25"><code>adc0eea</code></a>
README for updating actions/github-script from v7 to v8</li>
<li><a
href="https://github.com/actions/github-script/commit/20fe497b3fe0c7be8aae5c9df711ac716dc9c425"><code>20fe497</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/github-script/issues/637">#637</a>
from actions/node24</li>
<li><a
href="https://github.com/actions/github-script/commit/e7b7f222b11a03e8b695c4c7afba89a02ea20164"><code>e7b7f22</code></a>
update licenses</li>
<li><a
href="https://github.com/actions/github-script/commit/2c81ba05f308415d095291e6eeffe983d822345b"><code>2c81ba0</code></a>
Update Node.js version support to 24.x</li>
<li><a
href="https://github.com/actions/github-script/commit/f28e40c7f34bde8b3046d885e986cb6290c5673b"><code>f28e40c</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/github-script/issues/610">#610</a>
from actions/nebuk89-patch-1</li>
<li><a
href="https://github.com/actions/github-script/commit/1ae9958572fde544457e4d51aed5ea044e8936f3"><code>1ae9958</code></a>
Update README.md</li>
<li>Additional commits viewable in <a
href="https://github.com/actions/github-script/compare/v7.0.1...ed597411d8f924073f98dfc5c65a23a2325f34cd">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/github-script&package-manager=github_actions&previous-version=7.0.1&new-version=8.0.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-25 13:39:19 +00:00
Balázs SzücsandAnthony Stirling 5cd9377bba [V2] feat(flatten): add optional rendering DPI parameter to settings in flatten (#5276)
# Description of Changes

This pull request adds support for specifying a custom rendering DPI for
PDF flattening, allowing users to control the output sharpness and file
size. The changes update the UI to include a new DPI input (when
appropriate), extend parameter handling, and ensure the DPI value is
passed to the backend if provided.

**User interface enhancements:**

* Added a `NumberInput` field in `FlattenSettings.tsx` for users to
specify the rendering DPI, shown only when "Flatten only forms" is not
selected. The input includes label, help text, placeholder, and value
constraints.
* Updated the English locale file to add translation strings for the
rendering DPI input label, help text, and placeholder.

**Parameter and data handling:**

* Extended the `FlattenParameters` interface and default parameters to
include an optional `renderDpi` property.
* Modified the form data builder in `useFlattenOperation.ts` to append
the `renderDpi` value to the request if specified.
* Imported `NumberInput` from the Mantine UI library to support the new
DPI input field in the settings component.
<img width="363" height="998" alt="image"
src="https://github.com/user-attachments/assets/9a59f379-f1d3-4827-8a9c-f34c0bfcfada"
/>


<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [X] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [X] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [X] I have performed a self-review of my own code
- [X] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [X] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [X] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Signed-off-by: Balázs Szücs <bszucs1209@gmail.com>
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2025-12-25 01:27:53 +00:00
Balázs SzücsandReece Browne a3e1cc8393 [V2] refactor(ui): replace native inputs with Mantine components (#4898)
# Description of Changes

#### Before
<img width="779" height="768" alt="image"
src="https://github.com/user-attachments/assets/a6d63bac-e27d-4bd8-b44e-fa72333f4c59"
/>


#### After
<img width="779" height="768" alt="image"
src="https://github.com/user-attachments/assets/f39d8d92-802a-40ff-a7db-2b6a187b6847"
/>


<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Signed-off-by: Balázs Szücs <bszucs1209@gmail.com>
Co-authored-by: Reece Browne <74901996+reecebrowne@users.noreply.github.com>
2025-12-25 01:23:49 +00:00
Balázs Szücs 1810a12cf3 feat(rotate): add keyboard shortcuts for rotating with arrow keys (#5294)
# Description of Changes

This pull request adds keyboard shortcut support to the `RotateSettings`
component, allowing users to rotate using the left and right arrow keys.
This improves accessibility and user experience when interacting with
rotation controls.

**Enhanced keyboard interaction:**

* Added a `useEffect` hook to listen for `ArrowLeft` and `ArrowRight`
key presses, triggering the `rotateAnticlockwise` and `rotateClockwise`
methods on the `parameters` object, respectively.


<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

Signed-off-by: Balázs Szücs <bszucs1209@gmail.com>
2025-12-24 22:31:41 +00:00
dependabot[bot] c3cd8c48b5 build(deps): bump imageioVersion from 3.12.0 to 3.13.0 (#5295)
[//]: # (dependabot-start)
⚠️  **Dependabot is rebasing this PR** ⚠️ 

Rebasing might not happen immediately, so don't worry if this takes some
time.

Note: if you make any changes to this PR yourself, they will take
precedence over the rebase.

---

[//]: # (dependabot-end)

Bumps `imageioVersion` from 3.12.0 to 3.13.0.
Updates `com.twelvemonkeys.imageio:imageio-batik` from 3.12.0 to 3.13.0

Updates `com.twelvemonkeys.imageio:imageio-bmp` from 3.12.0 to 3.13.0

Updates `com.twelvemonkeys.imageio:imageio-jpeg` from 3.12.0 to 3.13.0

Updates `com.twelvemonkeys.imageio:imageio-tiff` from 3.12.0 to 3.13.0

Updates `com.twelvemonkeys.imageio:imageio-webp` from 3.12.0 to 3.13.0

Updates `com.twelvemonkeys.imageio:imageio-psd` from 3.12.0 to 3.13.0


Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-24 22:13:38 +00:00
dependabot[bot] 2502a8def0 build(deps): bump logback from 1.5.22 to 1.5.23 (#5298)
Bumps `logback` from 1.5.22 to 1.5.23.
Updates `ch.qos.logback:logback-core` from 1.5.22 to 1.5.23
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/qos-ch/logback/releases">ch.qos.logback:logback-core's
releases</a>.</em></p>
<blockquote>
<h2>Logback 1.5.23</h2>
<p><strong>2025-12-21 Release of logback version 1.5.23</strong></p>
<p>• In response to <a
href="https://redirect.github.com/qos-ch/logback/issues/959">issues/959</a>
file name collisions are detected at configuration time by analyzing the
configuration file and no longer at run time. This avoids the
<code>ConcurrentModificationException</code> reported in the issue.</p>
<p>• ZIP and XZ compression now use a <code>BufferedOutputStream</code>
when writing to the compressed file. This issue was reported in <a
href="https://redirect.github.com/qos-ch/logback/issues/988">issues/988</a>.</p>
<p>• A bit-wise identical binary of this version can be reproduced by
building from source code at commit
0bcc3feb54a6d99caac70969ee5f8334aad1fbaf associated with the tag
v_1.5.23. Release built using Java &quot;21&quot; 2023-10-17 LTS build
21.0.1.+12-LTS-29 under Linux Debian 11.6.</p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/qos-ch/logback/commit/0bcc3feb54a6d99caac70969ee5f8334aad1fbaf"><code>0bcc3fe</code></a>
prepare release 1.5.23</li>
<li><a
href="https://github.com/qos-ch/logback/commit/4627dbd618cbb2365a09c8013ec3fc00d349743e"><code>4627dbd</code></a>
better to use BufferedOutputStream during ZIP and XZ compression,
especially ...</li>
<li><a
href="https://github.com/qos-ch/logback/commit/299f091d3211ad38869aadadbf7b2f66f231ad52"><code>299f091</code></a>
add collision test in presence of conditional processing</li>
<li><a
href="https://github.com/qos-ch/logback/commit/b446f3f06188f4041cea827832ffb8a90fb07241"><code>b446f3f</code></a>
In Context, remove collision map</li>
<li><a
href="https://github.com/qos-ch/logback/commit/a3eb14df4886ce8c398f6486b22ea77ad45ba9af"><code>a3eb14d</code></a>
in response to issues/959, collision detection is now done by
FileCollisionAn...</li>
<li><a
href="https://github.com/qos-ch/logback/commit/681b2be7e11dc46f883ead7ed4603dbad92812ae"><code>681b2be</code></a>
remove unused method, minor comment edits</li>
<li><a
href="https://github.com/qos-ch/logback/commit/17a3edfccc6021cfdd6f0ff52bfd2ae8bcc2c393"><code>17a3edf</code></a>
start work on 1.5.23-SNAPSHOT</li>
<li>See full diff in <a
href="https://github.com/qos-ch/logback/compare/v_1.5.22...v_1.5.23">compare
view</a></li>
</ul>
</details>
<br />

Updates `ch.qos.logback:logback-classic` from 1.5.22 to 1.5.23
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/qos-ch/logback/releases">ch.qos.logback:logback-classic's
releases</a>.</em></p>
<blockquote>
<h2>Logback 1.5.23</h2>
<p><strong>2025-12-21 Release of logback version 1.5.23</strong></p>
<p>• In response to <a
href="https://redirect.github.com/qos-ch/logback/issues/959">issues/959</a>
file name collisions are detected at configuration time by analyzing the
configuration file and no longer at run time. This avoids the
<code>ConcurrentModificationException</code> reported in the issue.</p>
<p>• ZIP and XZ compression now use a <code>BufferedOutputStream</code>
when writing to the compressed file. This issue was reported in <a
href="https://redirect.github.com/qos-ch/logback/issues/988">issues/988</a>.</p>
<p>• A bit-wise identical binary of this version can be reproduced by
building from source code at commit
0bcc3feb54a6d99caac70969ee5f8334aad1fbaf associated with the tag
v_1.5.23. Release built using Java &quot;21&quot; 2023-10-17 LTS build
21.0.1.+12-LTS-29 under Linux Debian 11.6.</p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/qos-ch/logback/commit/0bcc3feb54a6d99caac70969ee5f8334aad1fbaf"><code>0bcc3fe</code></a>
prepare release 1.5.23</li>
<li><a
href="https://github.com/qos-ch/logback/commit/4627dbd618cbb2365a09c8013ec3fc00d349743e"><code>4627dbd</code></a>
better to use BufferedOutputStream during ZIP and XZ compression,
especially ...</li>
<li><a
href="https://github.com/qos-ch/logback/commit/299f091d3211ad38869aadadbf7b2f66f231ad52"><code>299f091</code></a>
add collision test in presence of conditional processing</li>
<li><a
href="https://github.com/qos-ch/logback/commit/b446f3f06188f4041cea827832ffb8a90fb07241"><code>b446f3f</code></a>
In Context, remove collision map</li>
<li><a
href="https://github.com/qos-ch/logback/commit/a3eb14df4886ce8c398f6486b22ea77ad45ba9af"><code>a3eb14d</code></a>
in response to issues/959, collision detection is now done by
FileCollisionAn...</li>
<li><a
href="https://github.com/qos-ch/logback/commit/681b2be7e11dc46f883ead7ed4603dbad92812ae"><code>681b2be</code></a>
remove unused method, minor comment edits</li>
<li><a
href="https://github.com/qos-ch/logback/commit/17a3edfccc6021cfdd6f0ff52bfd2ae8bcc2c393"><code>17a3edf</code></a>
start work on 1.5.23-SNAPSHOT</li>
<li>See full diff in <a
href="https://github.com/qos-ch/logback/compare/v_1.5.22...v_1.5.23">compare
view</a></li>
</ul>
</details>
<br />


Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-24 22:12:10 +00:00
Balázs Szücs 54cd804319 [V2] feat(attachments): add PDF/A-3b conversion, attachment listing, renaming, and deletion (#5304)
# Description of Changes

This pull request introduces major improvements to the PDF attachment
API, adding new endpoints for listing, renaming, and deleting
attachments in PDFs, as well as improving error handling and content
negotiation. It also adds support for converting PDFs to PDF/A-3b format
when adding attachments and introduces stricter validation for
attachment uploads. The exception handling is improved to ensure
consistent JSON error responses, even when the client requests a PDF.

**API Feature Additions:**

* Added new endpoints in `AttachmentController` for listing
(`/list-attachments`), renaming (`/rename-attachment`), and deleting
(`/delete-attachment`) PDF attachments, with corresponding request and
response models: `ListAttachmentsRequest`, `RenameAttachmentRequest`,
`DeleteAttachmentRequest`, and `AttachmentInfo`.

* Enhanced the `/add-attachments` endpoint to optionally convert the
resulting PDF to PDF/A-3b format, controlled by a new `convertToPdfA3b`
flag in `AddAttachmentRequest`.

**Validation and Robustness:**

* Introduced strict validation for attachment uploads, enforcing
non-empty attachments, a maximum size per attachment (50 MB), and a
total size limit (200 MB).

**Content Negotiation:**

* Updated `WebMvcConfig` to configure content negotiation, allowing both
PDF and JSON responses, and preventing 406 errors when clients request
PDFs but errors must be returned as JSON.


<img width="370" height="997" alt="image"
src="https://github.com/user-attachments/assets/571504d4-e97e-4b30-ae97-3defba217b47"
/>
<img width="1415" height="649" alt="image"
src="https://github.com/user-attachments/assets/bb8863fc-0be8-4bf2-af7d-73a229010f9a"
/>
<img width="1415" height="649" alt="image"
src="https://github.com/user-attachments/assets/68092672-5be5-4ef7-9cbc-1fb008b728e1"
/>
<img width="1415" height="649" alt="image"
src="https://github.com/user-attachments/assets/c4b0eda5-2573-4e38-8284-c077acb83f7f"
/>



<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [X] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [X] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [X] I have performed a self-review of my own code
- [X] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [X] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [X] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Signed-off-by: Balázs Szücs <bszucs1209@gmail.com>
2025-12-24 21:35:36 +00:00
1b0a1e938e added a flag to hide settings (#5263)
Added an optional flag in settings.yml to hide the settings button in no
login servers. When hidden, users can no longer:

- Open the Settings modal at all (gear button is hidden)
- Change General preferences (tool picker mode, hide unavailable
tools/conversions, auto‑unzip and file limit)
- Configure keyboard shortcuts (Hotkeys / Keyboard Shortcuts section)
- Use the in‑app update checker UI (see current/latest version, check
for updates, view update details)
- Note: When enableLogin === true, the flag is ignored and the Settings
button remains visible.

---------

Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
Co-authored-by: Reece Browne <74901996+reecebrowne@users.noreply.github.com>
2025-12-24 20:56:22 +00:00
Balázs Szücs 69b035c6e1 [V2] feat(security): add PDF standards verification feature using veraPDF (#4874)
# Description of Changes
- Implemented `PDFVerificationRequest` and `PDFVerificationResult`
models for validation requests and responses
- Developed `VeraPDFService` to validate PDFs against specific or
auto-detected standards
- Added `VerifyPDFController` with an endpoint for PDF verification
- Integrated veraPDF dependencies into project build file
- Deprecated unused `/verify-pdf` form in `SecurityWebController`
- Updated `EndpointConfiguration` to include the new `verify-pdf`
endpoint

This PR introduces a PDF standards verification feature to Stirling-PDF,
powered by the industry-standard veraPDF validation library. This
feature enables users to validate PDF files against multiple PDF
standards including PDF/A (archival), PDF/UA (accessibility), and WTPDF
standards.

### 1. PDF Standards Verification Endpoint
- New API Endpoint: `/api/v1/security/verify-pdf`
- Validates PDF files against multiple standards:
  - PDF/A (1b, 2a, 2b, 2u, 3a, 3b, 3u, 4, 4e, 4f) - Archival standards
  - PDF/UA-1 and PDF/UA-2 - Universal Accessibility standards
  - WTPDF - Well-Tagged PDF standards
- Auto-detection: Automatically detects and validates all standards
declared in the PDF's XMP metadata


### 2. Validation Results
The verification returns detailed JSON results including:
- Compliance status: Whether the PDF meets the standard requirements
- Declared vs validated standards: Shows what the PDF claims to be vs
what it actually is
- Categorized issues:
  - Errors: Critical compliance failures that prevent certification
  - Warnings: Non-critical issues and recommendations
- Detailed issue information:
  - Rule IDs from the specification
  - Descriptive error messages
  - Location within the PDF where the issue occurs
  - Specification references (clause numbers, test numbers)

### 3. Detect Issue Classification
Implements intelligent classification of validation issues:
- Errors: Issues that prevent standard compliance (font problems, color
space issues, structural problems)
- Warnings: Recommended but not required elements (metadata
recommendations, optional features)
- Classification based on:
  - Rule ID patterns
  - Clause number prefixes
  - Message content analysis

### New Files Added

#### Controllers
- VerifyPDFController.java: REST API controller handling PDF
verification requests
  - Handles multipart file uploads
  - Supports both single-standard and auto-detection modes
- Comprehensive error handling for encrypted PDFs, parsing errors, and
validation failures

#### Models
- PDFVerificationRequest.java: Request model for verification API
  - Extends standard PDFFile model
  - Optional `standard` parameter for manual standard selection
  
- PDFVerificationResult.java: Response model containing validation
results
  - Includes standard information and validation profile details
  - Separate lists for errors and warnings
  - Nested `ValidationIssue` class for detailed issue reporting

#### Services
- VeraPDFService.java: Core service implementing veraPDF integration
  - Initializes veraPDF Greenfield engine
  - Extracts declared PDF/A standards from XMP metadata
  - Performs validation against specified or detected standards
  - Converts veraPDF results to application-specific format
  - Implements smart issue classification logic


### Endpoint Configuration Updates

#### EndpointConfiguration.java
- Added `verify-pdf` to the Security group
- Added `verify-pdf` to the Java group (no external tools required)
- Created new veraPDF dependency group for endpoint availability
tracking
- Updated `isToolGroup()` method to recognize veraPDF as a tool
dependency


### Supported Standards

#### PDF/A (Archival)
- PDF/A-1 (a, b): ISO 19005-1:2005
- PDF/A-2 (a, b, u): ISO 19005-2:2011
- PDF/A-3 (a, b, u): ISO 19005-3:2012
- PDF/A-4 (standard, e, f): ISO 19005-4:2020

#### PDF/UA (Universal Accessibility)
- PDF/UA-1: ISO 14289-1:2014
- PDF/UA-2: ISO 14289-2 (latest)

#### WTPDF (Well-Tagged PDF)
- WTPDF 1.0: Tagged PDF for accessibility and structure

### Security Considerations

The following test scenarios should be validated:

1. Valid PDF/A documents (should return compliant)
2. Non-compliant PDF/A documents (should return errors)
3. PDFs without PDF/A declaration (should detect and report)
4. PDF/UA documents (should validate accessibility)
5. Encrypted PDFs (should return appropriate error)
6. Mixed standards (PDF/A + PDF/UA) (should validate both)
7. Empty standard parameter (should auto-detect)
8. Invalid standard parameter (should return error)

### API Usage Examples

```bash
curl -X POST http://localhost:8080/api/v1/security/verify-pdf \
  -F "fileInput=@document.pdf"
```


### Example Response
```json
[
  {
    "standard": "3b",
    "standardName": "PDF/A-ISO 19005-3:2012B compliant",
    "validationProfile": "3b",
    "validationProfileName": "PDF/A-ISO 19005-3:2012B",
    "complianceSummary": "PDF/A-ISO 19005-3:2012B compliant",
    "declaredPdfa": true,
    "compliant": true,
    "totalFailures": 0,
    "totalWarnings": 0,
    "failures": [],
    "warnings": []
  }
]
```

```json
[
  {
    "standard": "2b",
    "standardName": "PDF/A-ISO 19005-2:2011B with errors",
    "validationProfile": "2b",
    "validationProfileName": "PDF/A-ISO 19005-2:2011B",
    "complianceSummary": "PDF/A-ISO 19005-2:2011B with errors",
    "declaredPdfa": true,
    "compliant": false,
    "totalFailures": 2,
    "totalWarnings": 0,
    "failures": [
      {
        "ruleId": "RuleId [specification=ISO 19005-2:2011, clause=6.2.11.4.1, testNumber=1]",
        "message": "The font programs for all fonts used for rendering within a conforming file shall be embedded within that file, as defined in ISO 32000-1:2008, 9.9",
        "location": "Location [level=CosDocument, context=root/document[0]/pages[0](3 0 obj PDPage)/contentStream[0](105 0 obj PDContentStream)/operators[60]/font[0](ArialMT)]",
        "specification": "ISO 19005-2:2011",
        "clause": "6.2.11.4.1",
        "testNumber": "1"
      },
      {
        "ruleId": "RuleId [specification=ISO 19005-2:2011, clause=6.3.2, testNumber=1]",
        "message": "Except for annotation dictionaries whose Subtype value is Popup, all annotation dictionaries shall contain the F key",
        "location": "Location [level=CosDocument, context=root/document[0]/pages[0](3 0 obj PDPage)/annots[4](107 0 obj PDLinkAnnot)]",
        "specification": "ISO 19005-2:2011",
        "clause": "6.3.2",
        "testNumber": "1"
      }
    ],
    "warnings": []
  }
]
```

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Signed-off-by: Balázs Szücs <bszucs1209@gmail.com>
2025-12-23 22:29:30 +00:00
Balázs SzücsandCopilot 8164c56d33 [V2] feat(ocr): add option to invalidate digital signatures with warning tooltip (#5148)
# Description of Changes
This pull request adds support for invalidating digital signatures
during the OCR process, allowing users to process signed PDFs with OCR
at the cost of breaking their digital signatures. The feature is
integrated across the backend, frontend, and user interface, including
appropriate warnings to inform users of the consequences.

**Backend support for digital signature invalidation:**

- Added a new `invalidateDigitalSignatures` boolean field to the
`ProcessPdfWithOcrRequest` model, and updated the `OCRController` to
accept, pass, and handle this parameter. If enabled, the backend adds
the `--invalidate-digital-signatures` flag to the OCR command,
invalidating any digital signatures present in the PDF.

**Frontend and UI integration:**

- Added an "Invalidate digital signatures" option to the advanced OCR
settings in the UI, including a warning tooltip to inform users that
enabling this option will make the document's digital signatures
invalid.
- Updated the OCR form data builder to include the
`invalidateDigitalSignatures` parameter when submitting OCR requests.

**Localization and tooltips:**

- Added new translation strings and warning messages for the "Invalidate
digital signatures" option and its tooltip, ensuring users are clearly
informed about the consequences of enabling this feature.

<img width="649" height="994" alt="image"
src="https://github.com/user-attachments/assets/8d7f42cb-fc9e-4864-ad09-09ccb56347a1"
/>

Closes: #5142
<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Signed-off-by: Balázs Szücs <bszucs1209@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-23 10:54:53 +00:00
Balázs Szücs 098f2badec [V2] feat(getPdfInfo): add attachment, embedded file, and image info display (#5278)
# Description of Changes

This pull request enhances the display of PDF metadata in the "Other"
and "Per Page" sections by introducing more detailed and user-friendly
rendering of attachments, embedded files, and images. The changes add
new helper functions for rendering these lists and extend the type
definitions to support richer information, improving the clarity and
usefulness of the UI.

**Improvements to PDF metadata rendering:**

* Added `renderAttachmentsList` and `renderEmbeddedFilesList` helper
functions in `OtherSection.tsx` to display detailed information about
attachments and embedded files, including name, description, file size,
MIME type, and dates. These replace the generic list renderer for these
fields.
* Added `renderImagesList` helper function in `PerPageSection.tsx` to
display images with their name, dimensions, and color space, replacing
the generic list renderer for images.

**Type definition enhancements:**

* Extended `PdfEmbeddedFileInfo` and `PdfAttachmentInfo` in
`getPdfInfo.ts` to include additional fields: `FileSize`, `MimeType`,
`CreationDate`, and `ModificationDate` for embedded files, and
`FileSize` for attachments.

<img width="1920" height="998" alt="image"
src="https://github.com/user-attachments/assets/249409c1-4fd5-4599-b8f9-07093034cb55"
/>

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [X] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [X] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [X] I have performed a self-review of my own code
- [X] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [X] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [X] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Signed-off-by: Balázs Szücs <bszucs1209@gmail.com>
2025-12-22 23:05:52 +00:00
Anthony Stirling d2a4054120 Revert "[ImgBot] Optimize images" (#5293)
Reverts Stirling-Tools/Stirling-PDF#5290
2025-12-22 22:10:23 +00:00
Balázs Szücs 7baf503d60 [V2] feat(split): add split mode options and custom page selection to section split (#5277)
# Description of Changes


This pull request adds a new "Split Mode" feature to the "Split by
Sections" tool, allowing users to customize how pages are split,
including options to exclude the first/last pages or specify custom
pages. The changes include updates to the UI, form data handling,
parameter definitions, and validation logic to support these new
options.

**Split by Sections: New Split Mode Feature**

*UI Enhancements:*
- Added a `Radio.Group` in `SplitSettings.tsx` to let users choose the
split mode (split all, exclude first/last, or custom pages), and a
conditional `TextInput` for custom page numbers when "Custom" is
selected.

*Localization:*
- Updated `translation.toml` with new labels and descriptions for the
split mode options and custom page numbers.

*Parameter Handling:*
- Extended the `SplitParameters` interface to include `splitMode` and
`customPages`, and set their default values.

*Form Submission:*
- Modified `buildSplitFormData` to include the selected split mode and
custom page numbers (if applicable) in the form data sent to the
backend.

*Validation Logic:*
- Updated the validation logic in `useSplitParameters` to require custom
page numbers when the custom split mode is selected.

<img width="363" height="998" alt="image"
src="https://github.com/user-attachments/assets/c9f781a6-cf7e-4f3d-8514-57cfe468e0c8"
/>




<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [X] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [X] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [X] I have performed a self-review of my own code
- [X] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [X] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [X] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Signed-off-by: Balázs Szücs <bszucs1209@gmail.com>
2025-12-22 19:28:00 +00:00
EthanHealy01 d9622d0fac Cleanup/v2/text editor (#5169)
# Description of Changes

<img width="2056" height="1075" alt="Screenshot 2025-12-04 at 8 16
28 PM"
src="https://github.com/user-attachments/assets/9d2deacb-e471-4711-b9ac-9ef5a64b6f23"
/>
<img width="2056" height="1070" alt="Screenshot 2025-12-04 at 8 16
51 PM"
src="https://github.com/user-attachments/assets/07f07835-c973-4f56-80fb-5efc00ff1651"
/>
<img width="728" height="517" alt="Screenshot 2025-12-04 at 8 17 04 PM"
src="https://github.com/user-attachments/assets/8c59fae0-93a8-4da5-b2e4-9a89be651b7f"
/>
<img width="388" height="218" alt="Screenshot 2025-12-04 at 8 17 29 PM"
src="https://github.com/user-attachments/assets/997e9a68-6a3b-4033-bc45-04e3945f23d5"
/>


---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-12-22 18:25:25 +00:00
imgbot[bot]andImgBotApp ddba5a3eab [ImgBot] Optimize images (#5290)
## Beep boop. Your images are optimized!

Your image file size has been reduced by **20%** 🎉

<details>
<summary>
Details
</summary>

| File | Before | After | Percent reduction |
|:--|:--|:--|:--|
| /frontend/public/Login/LoginBackgroundPanel.png | 5.30kb | 2.21kb |
58.24% |
| /frontend/src-tauri/icons/icon.png | 19.17kb | 8.78kb | 54.22% |
| /frontend/public/modern-logo/logo512.png | 7.96kb | 4.49kb | 43.61% |
| /images/custom-home.png | 543.80kb | 329.50kb | 39.41% |
| /images/custom-home-dark.png | 456.09kb | 287.99kb | 36.86% |
| /frontend/public/modern-logo/logo192.png | 3.09kb | 2.06kb | 33.22% |
| /images/home.png | 98.13kb | 69.74kb | 28.93% |
| /frontend/public/classic-logo/logo512.png | 99.72kb | 78.32kb | 21.47%
|
| /frontend/public/modern-logo/Firstpage.png | 210.88kb | 169.27kb |
19.73% |
| /frontend/public/Login/Firstpage.png | 210.88kb | 169.27kb | 19.73% |
| /images/home-light.png | 56.34kb | 47.79kb | 15.19% |
| /frontend/public/Login/AddToPDF.png | 716.99kb | 627.65kb | 12.46% |
| /frontend/public/classic-logo/Firstpage.png | 405.84kb | 355.87kb |
12.31% |
| /frontend/public/classic-logo/logo192.png | 23.45kb | 21.19kb | 9.62%
|
| /frontend/src-tauri/icons/128x128@2x.png | 19.47kb | 18.27kb | 6.16% |
|
/frontend/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png
| 26.17kb | 24.76kb | 5.38% |
| /frontend/src-tauri/icons/Square284x284Logo.png | 22.54kb | 21.41kb |
5.02% |
| /frontend/src-tauri/icons/Square310x310Logo.png | 24.80kb | 23.74kb |
4.26% |
|
/frontend/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png
| 35.56kb | 34.13kb | 4.03% |
| /frontend/src-tauri/icons/ios/AppIcon-512@2x.png | 111.12kb | 108.22kb
| 2.61% |
|
/frontend/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png
| 16.67kb | 16.29kb | 2.28% |
| /frontend/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png | 11.38kb |
11.12kb | 2.21% |
| /frontend/src-tauri/icons/ios/AppIcon-60x60@3x.png | 12.33kb | 12.06kb
| 2.19% |
| /frontend/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png
| 14.63kb | 14.40kb | 1.60% |
| /frontend/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png |
14.63kb | 14.40kb | 1.60% |
| /frontend/src-tauri/icons/ios/AppIcon-76x76@2x.png | 10.34kb | 10.18kb
| 1.55% |
|
/frontend/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png
| 12.01kb | 11.88kb | 1.05% |
| /frontend/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png
| 10.59kb | 10.48kb | 0.99% |
| /frontend/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png |
10.59kb | 10.48kb | 0.99% |
| /frontend/src-tauri/icons/Square150x150Logo.png | 10.96kb | 10.87kb |
0.82% |
| /frontend/src-tauri/icons/ios/AppIcon-40x40@3x.png | 8.10kb | 8.04kb |
0.81% |
| /frontend/src-tauri/icons/ios/AppIcon-60x60@2x.png | 8.10kb | 8.04kb |
0.81% |
| /frontend/public/Login/SecurePDF.png | 228.99kb | 227.15kb | 0.80% |
| /frontend/src-tauri/icons/Square142x142Logo.png | 10.38kb | 10.33kb |
0.46% |
| /app/core/src/main/resources/static/api-wordmark.svg | 6.39kb | 6.39kb
| 0.11% |
| /frontend/public/modern-logo/StirlingPDFLogoGreyText.svg | 6.39kb |
6.39kb | 0.11% |
| | | | |
| **Total :** | **3,489.78kb** | **2,793.14kb** | **19.96%** |
</details>

---

[📝 docs](https://imgbot.net/docs) | [:octocat:
repo](https://github.com/imgbot/ImgBot) | [🙋🏾
issues](https://github.com/imgbot/ImgBot/issues) | [🏪
marketplace](https://github.com/marketplace/imgbot)

<i>~Imgbot - Part of [Optimole](https://optimole.com/) family</i>

Signed-off-by: ImgBotApp <ImgBotHelp@gmail.com>
Co-authored-by: ImgBotApp <ImgBotHelp@gmail.com>
2025-12-22 16:00:54 +00:00
Anthony Stirling 0bfb90cfc1 API mode UI (#5287)
# Description of Changes


<img width="1945" height="1895" alt="image"
src="https://github.com/user-attachments/assets/4ace2535-d0ec-44a7-b57c-df9bcdd5c5d7"
/>

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-12-22 15:49:27 +00:00
Balázs Szücs c454a72f08 [V2] feat(compress): add linearize option for fast web viewing (#5280)
# Description of Changes

This pull request adds a new "Linearize PDF" option to the PDF
compression tool, enabling users to optimize PDFs for fast web viewing.
The change includes updates to the user interface, parameters, and form
data handling to support this feature.

**User Interface Updates:**
- Added a new checkbox labeled "Linearize PDF for fast web viewing" to
the compression settings UI, allowing users to enable or disable PDF
linearization.

**Localization:**
- Introduced a new localization string for the "Linearize PDF for fast
web viewing" label in the English (UK) translation file.

**Parameter Handling:**
- Updated the `CompressParameters` interface and default parameters to
include a `linearize` boolean flag.
- Modified the form data builder to append the `linearize` parameter
when submitting compression requests.

<img width="366" height="998" alt="image"
src="https://github.com/user-attachments/assets/f1efcdd5-fb91-4232-94f7-0a72de86739f"
/>


<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [X] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [X] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [X] I have performed a self-review of my own code
- [X] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [X] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [X] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

Signed-off-by: Balázs Szücs <bszucs1209@gmail.com>
2025-12-22 15:47:12 +00:00
Balázs Szücs 34fd328444 [V2] feat(sign): add SVG support for signature image uploads (#5279)
# Description of Changes


This pull request updates the `ImageUploader` component to improve SVG
file support. The main changes ensure that SVG files can be uploaded and
are properly validated, even if their MIME type is not set as an image.

**SVG File Support Improvements:**

* Updated the file type validation in the `handleImageChange` function
to allow files ending with `.svg`, even if their MIME type is not
`image/*`.
* Modified the `accept` attribute in the `FileInput` component to
explicitly include `.svg` files, making SVG selection possible in the
file picker.

### Sign with SVG pics

<img width="1920" height="998" alt="image"
src="https://github.com/user-attachments/assets/9e24d83e-304c-4c7b-85b3-82798616ac5a"
/>


<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

Signed-off-by: Balázs Szücs <bszucs1209@gmail.com>
2025-12-22 15:46:26 +00:00
Anthony Stirling 738dd0645d Revert "Revert "Sync provider settings with settings.yml in config UI"" (#5273)
Reverts Stirling-Tools/Stirling-PDF#5271
2025-12-22 15:45:55 +00:00
dependabot[bot] e997f0d4ff build(deps): bump com.squareup.okhttp3:okhttp-bom from 5.3.1 to 5.3.2 (#4961)
[//]: # (dependabot-start)
⚠️  **Dependabot is rebasing this PR** ⚠️ 

Rebasing might not happen immediately, so don't worry if this takes some
time.

Note: if you make any changes to this PR yourself, they will take
precedence over the rebase.

---

[//]: # (dependabot-end)

Bumps
[com.squareup.okhttp3:okhttp-bom](https://github.com/square/okhttp) from
5.3.1 to 5.3.2.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/square/okhttp/blob/master/CHANGELOG.md">com.squareup.okhttp3:okhttp-bom's
changelog</a>.</em></p>
<blockquote>
<h2>Version 5.3.2</h2>
<p><em>2025-11-18</em></p>
<ul>
<li>
<p>Fix: Don't delay triggering timeouts. In Okio 3.16.0 we introduced a
regression that caused
timeouts to fire later than they were supposed to.</p>
</li>
<li>
<p>Upgrade: [Okio 3.16.4][okio_3_16_4].</p>
</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/square/okhttp/commit/75b9c267744cdd2e8b222d247052748ff0954304"><code>75b9c26</code></a>
Prepare for release 5.3.2.</li>
<li><a
href="https://github.com/square/okhttp/commit/ab48e5d86d0c0b16b3679b9b9522acf65db73da4"><code>ab48e5d</code></a>
Okio 3.16.4 (<a
href="https://redirect.github.com/square/okhttp/issues/9200">#9200</a>)</li>
<li><a
href="https://github.com/square/okhttp/commit/a9a4638b3d38b83782a959514b4183ddea0f071f"><code>a9a4638</code></a>
Prepare next development version.</li>
<li>See full diff in <a
href="https://github.com/square/okhttp/compare/parent-5.3.1...parent-5.3.2">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=com.squareup.okhttp3:okhttp-bom&package-manager=gradle&previous-version=5.3.1&new-version=5.3.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-22 15:45:30 +00:00
dependabot[bot] e137436656 build(deps): bump bouncycastleVersion from 1.82 to 1.83 (#5111)
Bumps `bouncycastleVersion` from 1.82 to 1.83.
Updates `org.bouncycastle:bcprov-jdk18on` from 1.82 to 1.83
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/bcgit/bc-java/blob/main/docs/releasenotes.html">org.bouncycastle:bcprov-jdk18on's
changelog</a>.</em></p>
<blockquote>
<!-- raw HTML omitted -->
<!-- raw HTML omitted -->
<!-- raw HTML omitted -->
<p><!-- raw HTML omitted --><!-- raw HTML omitted -->2.1.1 Version<!--
raw HTML omitted --><!-- raw HTML omitted -->
Release: 1.83<!-- raw HTML omitted -->
Date:      2025, November 27th.</p>
<!-- raw HTML omitted -->
<p><!-- raw HTML omitted --><!-- raw HTML omitted -->2.2.1 Version<!--
raw HTML omitted --><!-- raw HTML omitted -->
Release: 1.82<!-- raw HTML omitted -->
Date:      2025, 17th September.</p>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a
href="https://github.com/bcgit/bc-java/commits">compare view</a></li>
</ul>
</details>
<br />

Updates `org.bouncycastle:bcpkix-jdk18on` from 1.82 to 1.83
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/bcgit/bc-java/blob/main/docs/releasenotes.html">org.bouncycastle:bcpkix-jdk18on's
changelog</a>.</em></p>
<blockquote>
<!-- raw HTML omitted -->
<!-- raw HTML omitted -->
<!-- raw HTML omitted -->
<p><!-- raw HTML omitted --><!-- raw HTML omitted -->2.1.1 Version<!--
raw HTML omitted --><!-- raw HTML omitted -->
Release: 1.83<!-- raw HTML omitted -->
Date:      2025, November 27th.</p>
<!-- raw HTML omitted -->
<p><!-- raw HTML omitted --><!-- raw HTML omitted -->2.2.1 Version<!--
raw HTML omitted --><!-- raw HTML omitted -->
Release: 1.82<!-- raw HTML omitted -->
Date:      2025, 17th September.</p>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a
href="https://github.com/bcgit/bc-java/commits">compare view</a></li>
</ul>
</details>
<br />


Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-22 15:45:11 +00:00
dependabot[bot] 1649b9bffb build(deps): bump softprops/action-gh-release from 2.4.2 to 2.5.0 (#5134)
Bumps
[softprops/action-gh-release](https://github.com/softprops/action-gh-release)
from 2.4.2 to 2.5.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/softprops/action-gh-release/releases">softprops/action-gh-release's
releases</a>.</em></p>
<blockquote>
<h2>v2.5.0</h2>
<!-- raw HTML omitted -->
<h2>What's Changed</h2>
<h3>Exciting New Features 🎉</h3>
<ul>
<li>feat: mark release as draft until all artifacts are uploaded by <a
href="https://github.com/dumbmoron"><code>@​dumbmoron</code></a> in <a
href="https://redirect.github.com/softprops/action-gh-release/pull/692">softprops/action-gh-release#692</a></li>
</ul>
<h3>Other Changes 🔄</h3>
<ul>
<li>chore(deps): bump the npm group across 1 directory with 5 updates by
<a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/softprops/action-gh-release/pull/697">softprops/action-gh-release#697</a></li>
<li>chore(deps): bump actions/checkout from 5.0.0 to 5.0.1 in the
github-actions group by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/softprops/action-gh-release/pull/689">softprops/action-gh-release#689</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/dumbmoron"><code>@​dumbmoron</code></a>
made their first contribution in <a
href="https://redirect.github.com/softprops/action-gh-release/pull/692">softprops/action-gh-release#692</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/softprops/action-gh-release/compare/v2.4.2...v2.5.0">https://github.com/softprops/action-gh-release/compare/v2.4.2...v2.5.0</a></p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md">softprops/action-gh-release's
changelog</a>.</em></p>
<blockquote>
<h2>2.5.0</h2>
<h2>What's Changed</h2>
<h3>Exciting New Features 🎉</h3>
<ul>
<li>feat: mark release as draft until all artifacts are uploaded by <a
href="https://github.com/dumbmoron"><code>@​dumbmoron</code></a> in <a
href="https://redirect.github.com/softprops/action-gh-release/pull/692">softprops/action-gh-release#692</a></li>
</ul>
<h3>Other Changes 🔄</h3>
<ul>
<li>dependency updates</li>
</ul>
<h2>2.4.2</h2>
<h2>What's Changed</h2>
<h3>Exciting New Features 🎉</h3>
<ul>
<li>feat: Ensure generated release notes cannot be over 125000
characters by <a
href="https://github.com/BeryJu"><code>@​BeryJu</code></a> in <a
href="https://redirect.github.com/softprops/action-gh-release/pull/684">softprops/action-gh-release#684</a></li>
</ul>
<h3>Other Changes 🔄</h3>
<ul>
<li>dependency updates</li>
</ul>
<h2>2.4.1</h2>
<h2>What's Changed</h2>
<h3>Other Changes 🔄</h3>
<ul>
<li>fix(util): support brace expansion globs containing commas in
parseInputFiles by <a
href="https://github.com/Copilot"><code>@​Copilot</code></a> in <a
href="https://redirect.github.com/softprops/action-gh-release/pull/672">softprops/action-gh-release#672</a></li>
<li>fix: gracefully fallback to body when body_path cannot be read by <a
href="https://github.com/Copilot"><code>@​Copilot</code></a> in <a
href="https://redirect.github.com/softprops/action-gh-release/pull/671">softprops/action-gh-release#671</a></li>
</ul>
<h2>2.4.0</h2>
<h2>What's Changed</h2>
<h3>Exciting New Features 🎉</h3>
<ul>
<li>feat(action): respect working_directory for files globs by <a
href="https://github.com/stephenway"><code>@​stephenway</code></a> in <a
href="https://redirect.github.com/softprops/action-gh-release/pull/667">softprops/action-gh-release#667</a></li>
</ul>
<h2>2.3.4</h2>
<h2>What's Changed</h2>
<h3>Bug fixes 🐛</h3>
<ul>
<li>fix(action): handle 422 already_exists race condition by <a
href="https://github.com/stephenway"><code>@​stephenway</code></a> in <a
href="https://redirect.github.com/softprops/action-gh-release/pull/665">softprops/action-gh-release#665</a></li>
</ul>
<h3>Other Changes 🔄</h3>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/softprops/action-gh-release/commit/a06a81a03ee405af7f2048a818ed3f03bbf83c7b"><code>a06a81a</code></a>
release 2.5.0</li>
<li><a
href="https://github.com/softprops/action-gh-release/commit/7da8983734babbd3165bced7ac7add895656129f"><code>7da8983</code></a>
feat: mark release as draft until all artifacts are uploaded (<a
href="https://redirect.github.com/softprops/action-gh-release/issues/692">#692</a>)</li>
<li><a
href="https://github.com/softprops/action-gh-release/commit/87973286a4d52a3789332acb79caa97720ecde8d"><code>8797328</code></a>
chore(deps): bump actions/checkout in the github-actions group (<a
href="https://redirect.github.com/softprops/action-gh-release/issues/689">#689</a>)</li>
<li><a
href="https://github.com/softprops/action-gh-release/commit/1bfc62a71b435bb6eb12e454577ad67e07938581"><code>1bfc62a</code></a>
chore(deps): bump the npm group across 1 directory with 5 updates (<a
href="https://redirect.github.com/softprops/action-gh-release/issues/697">#697</a>)</li>
<li>See full diff in <a
href="https://github.com/softprops/action-gh-release/compare/5be0e66d93ac7ed76da52eca8bb058f665c3a5fe...a06a81a03ee405af7f2048a818ed3f03bbf83c7b">compare
view</a></li>
</ul>
</details>
<br />

<details>
<summary>Most Recent Ignore Conditions Applied to This Pull
Request</summary>

| Dependency Name | Ignore Conditions |
| --- | --- |
| softprops/action-gh-release | [>= 2.2.a, < 2.3] |
</details>


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=softprops/action-gh-release&package-manager=github_actions&previous-version=2.4.2&new-version=2.5.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-22 15:44:56 +00:00
stirlingbot[bot] c990ab3216 🤖 format everything with pre-commit by stirlingbot (#5144)
Auto-generated by [create-pull-request][1] with **stirlingbot**

[1]: https://github.com/peter-evans/create-pull-request

Signed-off-by: stirlingbot[bot] <stirlingbot[bot]@users.noreply.github.com>
Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com>
2025-12-22 15:44:38 +00:00
dependabot[bot] e6d3f20c36 build(deps): bump actions/stale from 10.1.0 to 10.1.1 (#5133)
[//]: # (dependabot-start)
⚠️  **Dependabot is rebasing this PR** ⚠️ 

Rebasing might not happen immediately, so don't worry if this takes some
time.

Note: if you make any changes to this PR yourself, they will take
precedence over the rebase.

---

[//]: # (dependabot-end)

Bumps [actions/stale](https://github.com/actions/stale) from 10.1.0 to
10.1.1.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/stale/releases">actions/stale's
releases</a>.</em></p>
<blockquote>
<h2>v10.1.1</h2>
<h2>What's Changed</h2>
<h3>Bug Fix</h3>
<ul>
<li>Add Missing Input Reading for <code>only-issue-types</code> by <a
href="https://github.com/Bibo-Joshi"><code>@​Bibo-Joshi</code></a> in <a
href="https://redirect.github.com/actions/stale/pull/1298">actions/stale#1298</a></li>
</ul>
<h3>Improvement</h3>
<ul>
<li>Improves error handling when rate limiting is disabled on GHES. by
<a
href="https://github.com/chiranjib-swain"><code>@​chiranjib-swain</code></a>
in <a
href="https://redirect.github.com/actions/stale/pull/1300">actions/stale#1300</a></li>
</ul>
<h3>Dependency Upgrades</h3>
<ul>
<li>Upgrade eslint-config-prettier from 8.10.0 to 10.1.8 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/stale/pull/1276">actions/stale#1276</a></li>
<li>Upgrade <code>@​types/node</code> from 20.10.3 to 24.2.0 and
document breaking changes in v10 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/stale/pull/1280">actions/stale#1280</a></li>
<li>Upgrade actions/publish-action from 0.3.0 to 0.4.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/stale/pull/1291">actions/stale#1291</a></li>
<li>Upgrade actions/checkout from 4 to 6 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/stale/pull/1306">actions/stale#1306</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/chiranjib-swain"><code>@​chiranjib-swain</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/stale/pull/1300">actions/stale#1300</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/stale/compare/v10...v10.1.1">https://github.com/actions/stale/compare/v10...v10.1.1</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/actions/stale/commit/997185467fa4f803885201cee163a9f38240193d"><code>9971854</code></a>
build(deps): bump actions/checkout from 4 to 6 (<a
href="https://redirect.github.com/actions/stale/issues/1306">#1306</a>)</li>
<li><a
href="https://github.com/actions/stale/commit/5611b9defa6b7799a950489b00163db69f7a3ece"><code>5611b9d</code></a>
build(deps): bump actions/publish-action from 0.3.0 to 0.4.0 (<a
href="https://redirect.github.com/actions/stale/issues/1291">#1291</a>)</li>
<li><a
href="https://github.com/actions/stale/commit/fad0de84e50d1aba7b0236cdaf0ea98a43286849"><code>fad0de8</code></a>
Improves error handling when rate limiting is disabled on GHES. (<a
href="https://redirect.github.com/actions/stale/issues/1300">#1300</a>)</li>
<li><a
href="https://github.com/actions/stale/commit/39bea7de61dd70ce4705a976f904f33d5e1e0f49"><code>39bea7d</code></a>
Add Missing Input Reading for <code>only-issue-types</code> (<a
href="https://redirect.github.com/actions/stale/issues/1298">#1298</a>)</li>
<li><a
href="https://github.com/actions/stale/commit/e46bbabb3ede15841d25946157759558dd16306e"><code>e46bbab</code></a>
build(deps-dev): bump <code>@​types/node</code> from 20.10.3 to 24.2.0
and document breakin...</li>
<li><a
href="https://github.com/actions/stale/commit/65d1d4804d3060875fff9f9fa8a49e27f71ce7f0"><code>65d1d48</code></a>
build(deps-dev): bump eslint-config-prettier from 8.10.0 to 10.1.8 (<a
href="https://redirect.github.com/actions/stale/issues/1276">#1276</a>)</li>
<li>See full diff in <a
href="https://github.com/actions/stale/compare/5f858e3efba33a5ca4407a664cc011ad407f2008...997185467fa4f803885201cee163a9f38240193d">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/stale&package-manager=github_actions&previous-version=10.1.0&new-version=10.1.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-22 15:22:21 +00:00
dependabot[bot] 79d5bc942d build(deps): bump actions/setup-python from 6.0.0 to 6.1.0 (#4992)
Bumps [actions/setup-python](https://github.com/actions/setup-python)
from 6.0.0 to 6.1.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/setup-python/releases">actions/setup-python's
releases</a>.</em></p>
<blockquote>
<h2>v6.1.0</h2>
<h2>What's Changed</h2>
<h3>Enhancements:</h3>
<ul>
<li>Add support for <code>pip-install</code> input by <a
href="https://github.com/gowridurgad"><code>@​gowridurgad</code></a> in
<a
href="https://redirect.github.com/actions/setup-python/pull/1201">actions/setup-python#1201</a></li>
<li>Add graalpy early-access and windows builds by <a
href="https://github.com/timfel"><code>@​timfel</code></a> in <a
href="https://redirect.github.com/actions/setup-python/pull/880">actions/setup-python#880</a></li>
</ul>
<h3>Dependency and Documentation updates:</h3>
<ul>
<li>Enhanced wording and updated example usage for
<code>allow-prereleases</code> by <a
href="https://github.com/yarikoptic"><code>@​yarikoptic</code></a> in <a
href="https://redirect.github.com/actions/setup-python/pull/979">actions/setup-python#979</a></li>
<li>Upgrade urllib3 from 1.26.19 to 2.5.0 and document breaking changes
in v6 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/setup-python/pull/1139">actions/setup-python#1139</a></li>
<li>Upgrade typescript from 5.4.2 to 5.9.3 and Documentation update by
<a href="https://github.com/dependabot"><code>@​dependabot</code></a> in
<a
href="https://redirect.github.com/actions/setup-python/pull/1094">actions/setup-python#1094</a></li>
<li>Upgrade actions/publish-action from 0.3.0 to 0.4.0 &amp;
Documentation update for pip-install input by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/setup-python/pull/1199">actions/setup-python#1199</a></li>
<li>Upgrade requests from 2.32.2 to 2.32.4 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/setup-python/pull/1130">actions/setup-python#1130</a></li>
<li>Upgrade prettier from 3.5.3 to 3.6.2 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/setup-python/pull/1234">actions/setup-python#1234</a></li>
<li>Upgrade <code>@​types/node</code> from 24.1.0 to 24.9.1 and update
macos-13 to macos-15-intel by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/setup-python/pull/1235">actions/setup-python#1235</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/yarikoptic"><code>@​yarikoptic</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/setup-python/pull/979">actions/setup-python#979</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/setup-python/compare/v6...v6.1.0">https://github.com/actions/setup-python/compare/v6...v6.1.0</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/actions/setup-python/commit/83679a892e2d95755f2dac6acb0bfd1e9ac5d548"><code>83679a8</code></a>
Bump <code>@​types/node</code> from 24.1.0 to 24.9.1 and update macos-13
to macos-15-intel ...</li>
<li><a
href="https://github.com/actions/setup-python/commit/bfc4944b43a5d84377eca3cf6ab5b7992ba61923"><code>bfc4944</code></a>
Bump prettier from 3.5.3 to 3.6.2 (<a
href="https://redirect.github.com/actions/setup-python/issues/1234">#1234</a>)</li>
<li><a
href="https://github.com/actions/setup-python/commit/97aeb3efb8a852c559869050c7fb175b4efcc8cf"><code>97aeb3e</code></a>
Bump requests from 2.32.2 to 2.32.4 in /<strong>tests</strong>/data (<a
href="https://redirect.github.com/actions/setup-python/issues/1130">#1130</a>)</li>
<li><a
href="https://github.com/actions/setup-python/commit/443da59188462e2402e2942686db5aa6723f4bed"><code>443da59</code></a>
Bump actions/publish-action from 0.3.0 to 0.4.0 &amp; Documentation
update for pi...</li>
<li><a
href="https://github.com/actions/setup-python/commit/cfd55ca82492758d853442341ad4d8010466803a"><code>cfd55ca</code></a>
graalpy: add graalpy early-access and windows builds (<a
href="https://redirect.github.com/actions/setup-python/issues/880">#880</a>)</li>
<li><a
href="https://github.com/actions/setup-python/commit/bba65e51ff35d50c6dbaaacd8a4681db13aa7cb4"><code>bba65e5</code></a>
Bump typescript from 5.4.2 to 5.9.3 and update docs/advanced-usage.md
(<a
href="https://redirect.github.com/actions/setup-python/issues/1094">#1094</a>)</li>
<li><a
href="https://github.com/actions/setup-python/commit/18566f86b301499665bd3eb1a2247e0849c64fa5"><code>18566f8</code></a>
Improve wording and &quot;fix example&quot; (remove 3.13) on testing
against pre-releas...</li>
<li><a
href="https://github.com/actions/setup-python/commit/2e3e4b15a884dc73a63f962bff250a855150a234"><code>2e3e4b1</code></a>
Add support for pip-install input (<a
href="https://redirect.github.com/actions/setup-python/issues/1201">#1201</a>)</li>
<li><a
href="https://github.com/actions/setup-python/commit/4267e283df95c05d9f16ece6624106f44613b489"><code>4267e28</code></a>
Bump urllib3 from 1.26.19 to 2.5.0 in /<strong>tests</strong>/data and
document breaking c...</li>
<li>See full diff in <a
href="https://github.com/actions/setup-python/compare/e797f83bcb11b83ae66e0230d6156d7c80228e7c...83679a892e2d95755f2dac6acb0bfd1e9ac5d548">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/setup-python&package-manager=github_actions&previous-version=6.0.0&new-version=6.1.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-22 15:21:50 +00:00
dependabot[bot] d652fc1d1c build(deps): bump actions/ai-inference from 1.2.8 to 2.0.4 (#5132)
Bumps [actions/ai-inference](https://github.com/actions/ai-inference)
from 1.2.8 to 2.0.4.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/ai-inference/releases">actions/ai-inference's
releases</a>.</em></p>
<blockquote>
<h2>v2.0.4</h2>
<h2>What's Changed</h2>
<ul>
<li>Clarify token requirements for MCP integration by <a
href="https://github.com/GulerSevil"><code>@​GulerSevil</code></a> in <a
href="https://redirect.github.com/actions/ai-inference/pull/139">actions/ai-inference#139</a></li>
<li>Pass GitHub MCP Tools by <a
href="https://github.com/maartenvandiemen"><code>@​maartenvandiemen</code></a>
in <a
href="https://redirect.github.com/actions/ai-inference/pull/142">actions/ai-inference#142</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/GulerSevil"><code>@​GulerSevil</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/ai-inference/pull/139">actions/ai-inference#139</a></li>
<li><a
href="https://github.com/maartenvandiemen"><code>@​maartenvandiemen</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/ai-inference/pull/142">actions/ai-inference#142</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/ai-inference/compare/v2...v2.0.4">https://github.com/actions/ai-inference/compare/v2...v2.0.4</a></p>
<h2>v2.0.3</h2>
<h2>What's Changed</h2>
<ul>
<li>Mock inference in CI by <a
href="https://github.com/sgoedecke"><code>@​sgoedecke</code></a> in <a
href="https://redirect.github.com/actions/ai-inference/pull/150">actions/ai-inference#150</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/ai-inference/compare/v2...v2.0.3">https://github.com/actions/ai-inference/compare/v2...v2.0.3</a></p>
<h2>v2.0.2</h2>
<h2>What's Changed</h2>
<ul>
<li>chore(deps): bump actions/checkout from 4 to 5 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/ai-inference/pull/94">actions/ai-inference#94</a></li>
<li>docs: update documentation on mcp usage by <a
href="https://github.com/FidelusAleksander"><code>@​FidelusAleksander</code></a>
in <a
href="https://redirect.github.com/actions/ai-inference/pull/93">actions/ai-inference#93</a></li>
<li>Clarify PAT requirement for github-mcp-token by <a
href="https://github.com/srt32"><code>@​srt32</code></a> in <a
href="https://redirect.github.com/actions/ai-inference/pull/147">actions/ai-inference#147</a></li>
<li>fix: do template substition after parsing prompt YAML by <a
href="https://github.com/dsanders11"><code>@​dsanders11</code></a> in <a
href="https://redirect.github.com/actions/ai-inference/pull/136">actions/ai-inference#136</a></li>
<li>feat: support modelParameters in prompt.yaml files by <a
href="https://github.com/dsanders11"><code>@​dsanders11</code></a> in <a
href="https://redirect.github.com/actions/ai-inference/pull/148">actions/ai-inference#148</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/FidelusAleksander"><code>@​FidelusAleksander</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/ai-inference/pull/93">actions/ai-inference#93</a></li>
<li><a href="https://github.com/srt32"><code>@​srt32</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/ai-inference/pull/147">actions/ai-inference#147</a></li>
<li><a
href="https://github.com/dsanders11"><code>@​dsanders11</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/ai-inference/pull/136">actions/ai-inference#136</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/ai-inference/compare/v2...v2.0.2">https://github.com/actions/ai-inference/compare/v2...v2.0.2</a></p>
<h2>v2.0.1</h2>
<h2>What's Changed</h2>
<ul>
<li>Parse inference response format defensively by <a
href="https://github.com/sgoedecke"><code>@​sgoedecke</code></a> in <a
href="https://redirect.github.com/actions/ai-inference/pull/97">actions/ai-inference#97</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/ai-inference/compare/v2...v2.0.1">https://github.com/actions/ai-inference/compare/v2...v2.0.1</a></p>
<h2>v2.0.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Node 24 by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/ai-inference/pull/79">actions/ai-inference#79</a></li>
<li>Pin two imported actions to a set sha by <a
href="https://github.com/garman"><code>@​garman</code></a> in <a
href="https://redirect.github.com/actions/ai-inference/pull/90">actions/ai-inference#90</a></li>
<li>Uses tmp library to ensure more secure tmp file creation by <a
href="https://github.com/JessRudder"><code>@​JessRudder</code></a> in <a
href="https://redirect.github.com/actions/ai-inference/pull/91">actions/ai-inference#91</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/ai-inference/pull/79">actions/ai-inference#79</a></li>
<li><a href="https://github.com/garman"><code>@​garman</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/ai-inference/pull/90">actions/ai-inference#90</a></li>
<li><a
href="https://github.com/JessRudder"><code>@​JessRudder</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/ai-inference/pull/91">actions/ai-inference#91</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/actions/ai-inference/commit/334892bb203895caaed82ec52d23c1ed9385151e"><code>334892b</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/ai-inference/issues/142">#142</a>
from maartenvandiemen/feature/pass-toolsets</li>
<li><a
href="https://github.com/actions/ai-inference/commit/bbe0ccb2444c1e35852645a7e65bf1d83941efdd"><code>bbe0ccb</code></a>
Fix failing tests</li>
<li><a
href="https://github.com/actions/ai-inference/commit/ca3b99ea743cc306cdaa68d07f817e930927c489"><code>ca3b99e</code></a>
Undo changes in tests.</li>
<li><a
href="https://github.com/actions/ai-inference/commit/8a5d2ea4a1b866720d3acc8bbbccd159e6ca7c3a"><code>8a5d2ea</code></a>
Merge branch 'main' into feature/pass-toolsets</li>
<li><a
href="https://github.com/actions/ai-inference/commit/112739fb15acc0a01a0d9ffc4637a0f5ca7dcc50"><code>112739f</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/ai-inference/issues/139">#139</a>
from GulerSevil/patch-1</li>
<li><a
href="https://github.com/actions/ai-inference/commit/f95554969e9a265f90a0c97938d04a6f319db7f3"><code>f955549</code></a>
Merge branch 'main' into patch-1</li>
<li><a
href="https://github.com/actions/ai-inference/commit/9e60aa0a3f27cab0e62286d74d9c77b04b1d0596"><code>9e60aa0</code></a>
Lint fix</li>
<li><a
href="https://github.com/actions/ai-inference/commit/02c6cc30ae592ce65ee356387748dfc2fd5f7993"><code>02c6cc3</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/ai-inference/issues/150">#150</a>
from actions/sgoedecke/mock-inference-in-ci</li>
<li><a
href="https://github.com/actions/ai-inference/commit/18d468666d48874954c853d63c0d9091bdfeb3b2"><code>18d4686</code></a>
fix: keep response-file temp file for downstream steps</li>
<li><a
href="https://github.com/actions/ai-inference/commit/fd73d0264cd240d895c114657b06fd1b952b97b6"><code>fd73d02</code></a>
Mock inference in CI</li>
<li>Additional commits viewable in <a
href="https://github.com/actions/ai-inference/compare/b81b2afb8390ee6839b494a404766bef6493c7d9...334892bb203895caaed82ec52d23c1ed9385151e">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/ai-inference&package-manager=github_actions&previous-version=1.2.8&new-version=2.0.4)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-22 15:21:27 +00:00
Balázs Szücs 22a1412242 [V2] feat(pdfa): add PDF/A-3b option with updated descriptions and translations (#5286)
# Description of Changes


This pull request adds support for the PDF/A-3b format in the PDF/A
conversion tool, updates the user interface to reflect this new option,
and enhances the translation strings to clarify the differences between
the PDF/A variants.

**PDF/A-3b support and UI update:**

* Added PDF/A-3b as an option in the PDF/A output format dropdown in
`ConvertToPdfaSettings.tsx`.
* Updated the PDF/A options note to mention that PDF/A-3b supports
embedded files, improving user guidance in both the UI and translation
strings.

**Localization improvements:**

* Added and updated relevant translation keys and values for PDF/A-3b
support and improved descriptions in both the TOML and JavaScript i18n
config files.


<img width="366" height="998" alt="image"
src="https://github.com/user-attachments/assets/c6db557a-2a9c-436d-bfb5-7360b33b0d36"
/>

Closes: #5173


<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [X] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [X] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [X] I have performed a self-review of my own code
- [X] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [X] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [X] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

Signed-off-by: Balázs Szücs <bszucs1209@gmail.com>
2025-12-22 15:17:47 +00:00
dependabot[bot] 0c3c0765a3 Bump logback from 1.5.21 to 1.5.22 (#5281)
Bumps `logback` from 1.5.21 to 1.5.22.
Updates `ch.qos.logback:logback-core` from 1.5.21 to 1.5.22
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/qos-ch/logback/releases">ch.qos.logback:logback-core's
releases</a>.</em></p>
<blockquote>
<h2>Logback 1.5.22</h2>
<p><strong>2025-12-11 Release of logback version 1.5.22</strong></p>
<p>• In order to prevent involuntary information leakage, Logback will
no longer output the value of a substituted variable, if the variable
name contains any of the case-insensitive strings &quot;password&quot;,
&quot;secret&quot; or &quot;confidential&quot;. This problem was
reported by Chintan Rohila in <a
href="https://redirect.github.com/qos-ch/logback/issues/986">issues/986</a>.</p>
<p>• Logback now takes the overridden <code>toString()</code> method of
<code>Throwable</code> subclasses into account when printing stack
traces. This issue was reported in <a
href="https://jira.qos.ch/browse/LOGBACK-543">LOGBACK-543</a> by Alvin
Chee, with a fix provided in <a
href="https://redirect.github.com/qos-ch/logback/pull/404">PR 404</a> by
Brett Kail.</p>
<p>• Instead of limit-counting guard, Logback now uses a tumbling-window
guard to rate limit internal error messages.</p>
<p>• A bit-wise identical binary of this version can be reproduced by
building from source code at commit
572379aabd2f672b49593e4020696c624541e5b0 associated with the tag
v_1.5.22. Release built using Java &quot;21&quot; 2023-10-17 LTS build
21.0.1.+12-LTS-29 under Linux Debian 11.6.</p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/qos-ch/logback/commit/572379aabd2f672b49593e4020696c624541e5b0"><code>572379a</code></a>
prepare release 1.5.22</li>
<li><a
href="https://github.com/qos-ch/logback/commit/39d17ea3b3381d08b181c300e27ca0713ffc20b8"><code>39d17ea</code></a>
fix status printing of variable substitution when the variable name
contains ...</li>
<li><a
href="https://github.com/qos-ch/logback/commit/75509a918665cc16a8d35ee4024be03e17c7147a"><code>75509a9</code></a>
fix PR 404, LOGBACK-543</li>
<li><a
href="https://github.com/qos-ch/logback/commit/8eb93569728ab33c50b963d42ea9fcd4269c502f"><code>8eb9356</code></a>
remove unused import</li>
<li><a
href="https://github.com/qos-ch/logback/commit/6131a3ad0af65a72df2e78d56424d9ac0fed8935"><code>6131a3a</code></a>
use a slightly more sophisticated guard for printing status
messages</li>
<li><a
href="https://github.com/qos-ch/logback/commit/9efca21c6e07feefa2a6ffb6b9b3807f357515e8"><code>9efca21</code></a>
add no-args constructor to support various serialization frameworks</li>
<li><a
href="https://github.com/qos-ch/logback/commit/1bea5804f8329a7e49a4197e34cc297ad46a597c"><code>1bea580</code></a>
minor comment edits</li>
<li><a
href="https://github.com/qos-ch/logback/commit/bd07fddf12b8b74d28d313a56e7357f6202d2449"><code>bd07fdd</code></a>
update angus, greenmail versions</li>
<li><a
href="https://github.com/qos-ch/logback/commit/aef993c64b4a7119f9e831fd4acaa7e470e267ca"><code>aef993c</code></a>
start work on 1.5.22-SNAPSHOT</li>
<li>See full diff in <a
href="https://github.com/qos-ch/logback/compare/v_1.5.21...v_1.5.22">compare
view</a></li>
</ul>
</details>
<br />

Updates `ch.qos.logback:logback-classic` from 1.5.21 to 1.5.22
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/qos-ch/logback/releases">ch.qos.logback:logback-classic's
releases</a>.</em></p>
<blockquote>
<h2>Logback 1.5.22</h2>
<p><strong>2025-12-11 Release of logback version 1.5.22</strong></p>
<p>• In order to prevent involuntary information leakage, Logback will
no longer output the value of a substituted variable, if the variable
name contains any of the case-insensitive strings &quot;password&quot;,
&quot;secret&quot; or &quot;confidential&quot;. This problem was
reported by Chintan Rohila in <a
href="https://redirect.github.com/qos-ch/logback/issues/986">issues/986</a>.</p>
<p>• Logback now takes the overridden <code>toString()</code> method of
<code>Throwable</code> subclasses into account when printing stack
traces. This issue was reported in <a
href="https://jira.qos.ch/browse/LOGBACK-543">LOGBACK-543</a> by Alvin
Chee, with a fix provided in <a
href="https://redirect.github.com/qos-ch/logback/pull/404">PR 404</a> by
Brett Kail.</p>
<p>• Instead of limit-counting guard, Logback now uses a tumbling-window
guard to rate limit internal error messages.</p>
<p>• A bit-wise identical binary of this version can be reproduced by
building from source code at commit
572379aabd2f672b49593e4020696c624541e5b0 associated with the tag
v_1.5.22. Release built using Java &quot;21&quot; 2023-10-17 LTS build
21.0.1.+12-LTS-29 under Linux Debian 11.6.</p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/qos-ch/logback/commit/572379aabd2f672b49593e4020696c624541e5b0"><code>572379a</code></a>
prepare release 1.5.22</li>
<li><a
href="https://github.com/qos-ch/logback/commit/39d17ea3b3381d08b181c300e27ca0713ffc20b8"><code>39d17ea</code></a>
fix status printing of variable substitution when the variable name
contains ...</li>
<li><a
href="https://github.com/qos-ch/logback/commit/75509a918665cc16a8d35ee4024be03e17c7147a"><code>75509a9</code></a>
fix PR 404, LOGBACK-543</li>
<li><a
href="https://github.com/qos-ch/logback/commit/8eb93569728ab33c50b963d42ea9fcd4269c502f"><code>8eb9356</code></a>
remove unused import</li>
<li><a
href="https://github.com/qos-ch/logback/commit/6131a3ad0af65a72df2e78d56424d9ac0fed8935"><code>6131a3a</code></a>
use a slightly more sophisticated guard for printing status
messages</li>
<li><a
href="https://github.com/qos-ch/logback/commit/9efca21c6e07feefa2a6ffb6b9b3807f357515e8"><code>9efca21</code></a>
add no-args constructor to support various serialization frameworks</li>
<li><a
href="https://github.com/qos-ch/logback/commit/1bea5804f8329a7e49a4197e34cc297ad46a597c"><code>1bea580</code></a>
minor comment edits</li>
<li><a
href="https://github.com/qos-ch/logback/commit/bd07fddf12b8b74d28d313a56e7357f6202d2449"><code>bd07fdd</code></a>
update angus, greenmail versions</li>
<li><a
href="https://github.com/qos-ch/logback/commit/aef993c64b4a7119f9e831fd4acaa7e470e267ca"><code>aef993c</code></a>
start work on 1.5.22-SNAPSHOT</li>
<li>See full diff in <a
href="https://github.com/qos-ch/logback/compare/v_1.5.21...v_1.5.22">compare
view</a></li>
</ul>
</details>
<br />


Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-22 15:15:40 +00:00
+17 68ed54e398 V1 merge (#5193)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Balázs Szücs <bszucs1209@gmail.com>
Signed-off-by: stirlingbot[bot] <stirlingbot[bot]@users.noreply.github.com>
Co-authored-by: ConnorYoh <40631091+ConnorYoh@users.noreply.github.com>
Co-authored-by: Connor Yoh <connor@stirlingpdf.com>
Co-authored-by: OUNZAR Aymane <aymane.ounzar@imt-atlantique.net>
Co-authored-by: YAOU Reda <yaoureda24@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com>
Co-authored-by: Balázs Szücs <127139797+balazs-szucs@users.noreply.github.com>
Co-authored-by: Ludy <Ludy87@users.noreply.github.com>
Co-authored-by: tkymmm <136296842+tkymmm@users.noreply.github.com>
Co-authored-by: Peter Dave Hello <hsu@peterdavehello.org>
Co-authored-by: albanobattistella <34811668+albanobattistella@users.noreply.github.com>
Co-authored-by: PingLin8888 <88387490+PingLin8888@users.noreply.github.com>
Co-authored-by: FdaSilvaYY <FdaSilvaYY@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: OteJlo <106060728+OteJlo@users.noreply.github.com>
Co-authored-by: Angel <41905618+TheShadowAngel@users.noreply.github.com>
Co-authored-by: Ricardo Catarino <ricardomicc@gmail.com>
Co-authored-by: Luis Antonio Argüelles González <luis.arguelles@encora.com>
Co-authored-by: Dawid Urbański <31166488+urbaned121@users.noreply.github.com>
Co-authored-by: Stephan Paternotte <Stephan-P@users.noreply.github.com>
Co-authored-by: Leonardo Santos Paulucio <leonardo.paulucio@hotmail.com>
Co-authored-by: hamza khalem <72972114+hamzakhalem@users.noreply.github.com>
Co-authored-by: IT Creativity + Art Team <admin@it-playground.net>
Co-authored-by: Reece Browne <74901996+reecebrowne@users.noreply.github.com>
Co-authored-by: James Brunton <jbrunton96@gmail.com>
Co-authored-by: Victor Villarreal <133383186+vvillarreal-cfee@users.noreply.github.com>
2025-12-21 10:40:32 +00:00
Anthony Stirling a5dcdd5bd9 Revert "Sync provider settings with settings.yml in config UI" (#5271)
Reverts Stirling-Tools/Stirling-PDF#5270
2025-12-20 22:21:45 +00:00
Ludy 5eafbf9e51 Sync provider settings with settings.yml in config UI (#5270)
# Description of Changes

This pull request improves the handling of provider settings in the
`ProviderCard` component to ensure settings are always in sync with the
latest values from `settings.yml`, and enhances how default values are
initialized for provider fields. It also updates the usage of
`ProviderCard` in the admin connections section to explicitly pass
provider settings.

**Provider settings synchronization and initialization:**

* Added a `useEffect` in `ProviderCard` to keep `localSettings` in sync
with incoming `settings` prop, ensuring that changes from `settings.yml`
are reflected in the UI.
* Improved default value initialization in `ProviderCard`: when opening
an unconfigured provider, it now merges defaults with any existing
values from `settings.yml`, rather than overwriting them.

**Integration with AdminConnectionsSection:**

* Updated `AdminConnectionsSection` to explicitly pass provider settings
to `ProviderCard` via the `settings` prop, ensuring the component always
receives the correct configuration.

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-12-20 22:08:35 +00:00
Dario Ghunney Ware a1317f0e01 Bumping version (#5268) 2025-12-18 20:43:10 +00:00
Reece Browne 3529849bca Feature/annotations (#5260) 2025-12-18 15:47:54 +00:00
Stephan Paternotte 49bea34576 Update translation.toml (#5254)
Updated translations of latest additions,
Saintized lots of accented letters,
Brought consistency in formal addressing of the user
2025-12-17 13:37:47 +00:00
Anthony StirlingandConnorYoh f9a44c4da4 Saml fixes (#5256)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Co-authored-by: ConnorYoh <40631091+ConnorYoh@users.noreply.github.com>
2025-12-17 10:52:48 +00:00
Anthony Stirling 4ec75d4d8c allow static overrides (#5258)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-12-17 10:07:51 +00:00
Reece Browne 93ed05b054 Only allow dual page view when there is more than one page, update to… (#5246) 2025-12-16 16:02:22 +00:00
Reece BrowneandClaude Sonnet 4.5 195b1472e4 Bug/v2/viewer annotations (#5245)
Show uneditable annotations on viewer
show editable annotations layer when in annotation tools (sign, add
image, add text)
Remove draw tool from viewer (this is replaced wholesale in an upcoming
PR so it wasn't worth doing the work to ensure it worked with the new
annotation layer set up_)
refactoring work, mostly renaming variables we can use for all
annotation based tools that had sign specific names.
remove "tools" tooltip

---------

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-16 15:52:47 +00:00
James Brunton 340006ceea Add Sign Up functionality to desktop app (#5244)
# Description of Changes
Adds Sign Up with email to desktop app. SSO sign up will come in a
future PR.
2025-12-16 14:55:53 +00:00
Anthony Stirling d80e627899 Cache fix issues V2 (#5237)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-12-15 23:54:25 +00:00
Anthony Stirling 336ec34125 V2 Handle SSO account restrictions in account settings (#5225)
## Summary
- hide password and username update controls for SSO accounts and show a
managed-account notice
- prevent account update handlers from calling APIs when the user
authenticates via SSO
- expose authenticationType on the user session model and add
translations for new SSO messaging

## Testing
- not run (not requested)


------
[Codex
Task](https://chatgpt.com/codex/tasks/task_b_693ae8144148832888ecf128e66cd3ca)
2025-12-15 21:55:58 +00:00
Anthony StirlingandJames Brunton 5f72c05623 line art (#5052)
## Summary
- introduce a shared line art conversion interface and proprietary
ImageMagick-backed implementation
- have the compress controller optionally autowire the enterprise
service before running per-image line art processing
- remove ImageMagick command details from core by delegating conversions
through the proprietary service

## Testing
- not run (not requested)


------
[Codex
Task](https://chatgpt.com/codex/tasks/task_b_6928aecceaf083289a9269b1ca99307e)

---------

Co-authored-by: James Brunton <jbrunton96@gmail.com>
2025-12-15 11:14:10 +00:00
Anthony Stirling 33188815da Remove UserApi mapping from proprietary signature controller (#5239)
## Summary
- remove the UserApi composite annotation from the proprietary signature
controller to avoid duplicate request mappings

## Testing
- Not run (not requested)


------
[Codex
Task](https://chatgpt.com/codex/tasks/task_b_693af8cc8210832890f4787bae07d11f)
2025-12-15 10:43:36 +00:00
EthanHealy01andJames Brunton 0064c1866e Shorten onbaording (#5198)
Also added `enableDesktopInstallSlide` flag in `settings.yml` to hide
the download for desktop page in the onboarding.

---------

Co-authored-by: James Brunton <james@stirlingpdf.com>
2025-12-15 09:31:41 +00:00
Anthony Stirling 371d816ce7 Validate certificate inputs for cert signing (#5191)
## Summary
- validate required certificate inputs before loading keystores to
prevent null dereferences
- surface clear errors for missing PEM, PKCS12/PFX, and JKS uploads
during PDF signing
- add a unit test covering the missing PKCS12/PFX keystore scenario

## Testing
- ./gradlew :stirling-pdf:test --tests
stirling.software.SPDF.controller.api.security.CertSignControllerTest

------
[Codex
Task](https://chatgpt.com/codex/tasks/task_b_6934c8803d648328bf76b72a4f689c60)
2025-12-13 17:42:12 +00:00
Anthony Stirling 5f54308d2b Simplify V2 PR auto-deploy triggers (#5236)
## Summary
- deploy V2 environments for authorized PR authors without requiring
title or branch keywords
- update deployment messaging to reflect the new trigger policy

## Testing
- not run


------
[Codex
Task](https://chatgpt.com/codex/tasks/task_b_693c9ac446d88328b8ae04609caec787)
2025-12-13 16:51:16 +00:00
Reece Browne 6f7b8ce433 Bug/v2/static wasm (#5238)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-12-13 12:57:10 +00:00
James Brunton 69ffd29bb5 Fix German text for sign tool text entry (#5232)
# Description of Changes
Fix #5206

Required splitting out the logic for the text with font entry so that
the labels are configurable from the call-site instead of all using the
same ones for Sign.
2025-12-12 18:00:40 +00:00
Reece Browne f4cc87144d Fix language codes in picker (#5233)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-12-12 17:44:15 +00:00
Anthony Stirling c86e2d6840 Delete .github/README.md 2025-12-11 16:38:32 +00:00
Anthony Stirling 5f072f87bb Account change details (#5190)
## Summary
Accounts setting page to change a users password or username
Fix huge bug were users can see admin settings due to hard code
admin=true
## Testing
- not run (not requested)

------
[Codex
Task](https://chatgpt.com/codex/tasks/task_b_6934b8ecdbf08328a0951b46db77dfd2)
2025-12-11 13:56:35 +00:00
Anthony Stirling eb3e57577c Bump version from 2.1.2 to 2.1.3 (#5224)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-12-11 13:16:37 +00:00
Reece Browne f29d85565a Chore/v2/ctrlf (#5217)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-12-11 12:42:16 +00:00
Reece Browne ae72344317 Offline pdfium (#5213)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-12-11 11:23:20 +00:00
Reece Browne 6565a6ce18 Bug/v2/improved cache busting (#5107)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-12-11 11:19:55 +00:00
Ludy e26035c3b3 build(versioning): synchronize app version across Tauri and simulation configs (#5120)
# Description of Changes

- **What was changed**
- Added `groovy.json.JsonOutput` and `groovy.json.JsonSlurper` imports
to `build.gradle`.
- Introduced a reusable `writeIfChanged(File targetFile, String
newContent)` helper to avoid unnecessary file writes when content is
unchanged.
  - Added `updateTauriConfigVersion(String version)` to:
    - Parse `frontend/src-tauri/tauri.conf.json`.
    - Set the `version` field from `project.version`.
- Re-write the file as pretty-printed JSON (with a trailing line
separator) only if content actually changed.
- Added `updateSimulationVersion(File fileToUpdate, String version)` to:
- Locate the `appVersion: '<value>'` assignment via regex in simulation
files.
    - Replace the existing version with `project.version`.
- Fail the build with a clear `GradleException` if `appVersion` cannot
be found.
- Registered a new Gradle task `syncAppVersion` (group: `versioning`)
which:
    - Reads `project.version` as the canonical app version.
    - Updates `frontend/src-tauri/tauri.conf.json`.
- Updates `frontend/src/core/testing/serverExperienceSimulations.ts`.
- Updates
`frontend/src/proprietary/testing/serverExperienceSimulations.ts`.
- Updated the main `build` task so it now depends on `syncAppVersion` in
addition to `:stirling-pdf:bootJar` and `buildRestartHelper`.

- **Why the change was made**
- To ensure the desktop Tauri configuration and server experience
simulation configs consistently use the same application version as
defined in `project.version`.
- To remove manual version bumps in multiple files and eliminate the
risk of version mismatches between backend, desktop app, and
simulation/testing tooling.
- To minimize noise in commits and CI by only touching versioned files
when their content actually changes (using `writeIfChanged`).


---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-12-11 11:13:54 +00:00
ConnorYoh e474cc76ad Improved static upgrade flow (#5214)
<img width="996" height="621" alt="image"
src="https://github.com/user-attachments/assets/1ac87414-09ed-4307-8f7c-25984e0c89d1"
/>
<img width="608" height="351" alt="image"
src="https://github.com/user-attachments/assets/c271f75e-4844-4034-8905-007cc7ab1265"
/>
<img width="660" height="355" alt="image"
src="https://github.com/user-attachments/assets/34913b74-d4fa-418a-b098-fda48b41f0dd"
/>
<img width="1371" height="906" alt="image"
src="https://github.com/user-attachments/assets/35b61389-fd67-41b3-9969-e5409e53b362"
/>
<img width="639" height="450" alt="image"
src="https://github.com/user-attachments/assets/ae018bf3-0fcf-4221-892f-440d7325540a"
/>
<img width="963" height="599" alt="image"
src="https://github.com/user-attachments/assets/f6f67682-f43c-46f3-8632-16b209780b15"
/>
<img width="982" height="628" alt="image"
src="https://github.com/user-attachments/assets/45a7c171-3eb4-4271-a299-f3a6e78c1a52"
/>
2025-12-11 11:13:20 +00:00
Reece Browne 43eaa84a8f fix tooltips on tab (#5219)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-12-11 09:20:39 +00:00
James Brunton 2cd4175689 Fix Mac app not being able to open files with spaces in their name (#5218)
# Description of Changes
Fix #5189.

Fix Mac app not being able to open files with spaces in their name,
which was happening because the URL was not being decoded on input.
2025-12-11 08:55:37 +00:00
Dario Ghunney Ware d6a83fe6a1 Fix: SSO Login Page (#5220)
Users logging in via OAuth2 were redirected to Spring's default login
form instead of the React frontend login page. This happened because the
OAuth2 configuration used `.loginPage("/oauth2")` which pointed to the
old Thymeleaf template.

### Testing (if applicable)

- [x] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-12-10 19:46:48 +00:00
James Brunton 3c92cb7c2b Improve styling of quick access bar (#5197)
# Description of Changes

Currently, the Quick Access Bar only renders well in Chrome. It's got
all sorts of layout issues in Firefox and Safari. This PR attempts to
retain the recent changes to make the bar thinner etc. but make it work
better in all browsers.
2025-12-10 17:21:07 +00:00
James Brunton b83888c74a Make lite version of CI (#5188)
# Description of Changes
Add lite mode for CI which just runs the most important jobs for
deployment. This won't be used in this repo, but allows other repos
containing Stirling to easily disable jobs like desktop builds etc. if
they're unnecessary, without needing to deal with conflicts in the
files. They'll just need to set the repo variable `CI_PROFILE` to
`lite`. We have an upstream repo that we'd like these changes for.
2025-12-10 13:54:57 +00:00
Anthony Stirling 787d0d21c9 lang updates plus --include-existing flag (#5212)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-12-10 11:41:11 +00:00
Dario Ghunney Ware 7b26b184d1 Fix: Access to Swagger UI when login enabled (#5194)
Fixes for /swagger-ui/index.html & /v1/api-docs endpoints not being
accessible when login was enabled.

- `UserAuthenticationFilter.isPublicAuthEndpoint()` had gaps in its
check, missing `/v1/api-docs`
- Refactored `UserAuthenticationFilter` to use
`RequestUriUtils.isPublicAuthEndpoint()` instead of its own incorrect
method

Closes #5125 & #5028

---

### Testing (if applicable)

- [x] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-12-10 11:39:29 +00:00
Anthony Stirling f17ad56def Handle restricted language configuration fallback V2 (#5154)
## Summary
- restrict supported languages to the validated list from app-config
instead of always adding an extra fallback
- set the effective fallback locale to the preferred configured language
and switch away from disallowed selections automatically

## Testing
- ./gradlew build


------
[Codex
Task](https://chatgpt.com/codex/tasks/task_b_6930529bc6c08328a1ce05f7d1316e27)
2025-12-10 11:12:18 +00:00
Reece Browne de438d00e1 Fiix colours (#5211)
<img width="638" height="471" alt="image"
src="https://github.com/user-attachments/assets/c2352e25-6ee0-4726-9ce4-059870347ea6"
/>
<img width="638" height="450" alt="image"
src="https://github.com/user-attachments/assets/a17931ca-ffea-4dc3-a17a-629d98003da6"
/>
<img width="985" height="774" alt="image"
src="https://github.com/user-attachments/assets/b16203c6-a136-4fee-ba57-495f7b6e8c75"
/>
<img width="635" height="348" alt="image"
src="https://github.com/user-attachments/assets/4b4ab328-b2e4-442f-84a5-d947e97653ec"
/>
2025-12-10 11:10:34 +00:00
Anthony Stirling 6787169583 Handle composition input in PDF text editor (#5192)
## Summary
- track IME composition state in the PDF text editor to avoid
interrupting phonetic input methods
- update text syncing to occur after composition completes and skip
redundant updates mid-composition

## Testing
- npm run lint -- --max-warnings 0


------
[Codex
Task](https://chatgpt.com/codex/tasks/task_b_693744be74148328bd3bda9150de6e56)
2025-12-10 10:11:45 +00:00
Anthony Stirling 291e1a392b extra font support in text editor (#5208)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-12-10 10:11:16 +00:00
Anthony Stirling 7c5aa3685f Add configurable SMTP TLS/SSL options (#5204)
## Summary
- add optional STARTTLS and SSL-related fields to mail settings with
defaults matching prior behavior
- apply the new mail properties in the SMTP JavaMail configuration,
including trust and hostname verification overrides
- update mail settings template and tests to cover default behavior and
explicit TLS/SSL overrides
- clarify STARTTLS naming and sslTrust usage with examples in property
comments and the settings template
- default sslTrust to a wildcard when unset so TLS connections accept
any host by default unless tightened

## Testing
- ./gradlew :proprietary:test --tests
stirling.software.proprietary.security.service.MailConfigTest --console
plain

------
[Codex
Task](https://chatgpt.com/codex/tasks/task_b_693864b2a6648328ae75c7e88a726a65)
2025-12-10 10:10:54 +00:00
Anthony StirlingandCopilot Autofix powered by AI 9c03914edd Add admin password reset option for users (#5180)
## Summary
- add backend support for admins to reset user passwords and optionally
email notifications when SMTP is enabled
- surface mail capability in admin settings data for the UI
- add a shared change-password modal hooked into People and Team user
actions with random password generation and email options

## Testing
- not run (not requested)


------
[Codex
Task](https://chatgpt.com/codex/tasks/task_b_6934b978fe3c83289b5b95dec79b3d38)

---------

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2025-12-10 10:10:40 +00:00
James Brunton c980ee10c0 Backport fixes from SaaS (#5187)
# Description of Changes
- Add new skeleton loader style type (block - nothing's currently using
it but it might as well be available)
- Make Dev API overridable (and set to the new docs that actually work
while Swagger docs don't work properly)
2025-12-09 11:45:01 +00:00
James Brunton fa4d2bc09a Fix path to sample file in tour (#5186)
# Description of Changes
Fix path to sample file in tour
2025-12-09 11:43:18 +00:00
Anthony Stirling 7faf7e50fa Chang etext on intro (#5160)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-12-06 00:06:11 +00:00
EthanHealy01 bb201ef9c1 Chore/bump gradle version number (#5176)
bump version number
2025-12-05 23:22:32 +00:00
82dbcfbb9b SSO login fix (#5167)
Fixes bug where SSO login with custom providers caused an
`InvalidClientRegistrationIdException: Invalid Client Registration with
Id: oidc` errors.

  Root Cause:
- Backend: Redirect URI was hardcoded to `/login/oauth2/code/oidc`
regardless of provider registration ID
- Frontend: Unknown providers were mapped back to 'oidc' instead of
using actual provider ID

Closes #5141

---------

Co-authored-by: Anthony Stirling <77850077+frooodle@users.noreply.github.com>
Co-authored-by: Keon Chen <66115421+keonchennl@users.noreply.github.com>
2025-12-05 23:19:41 +00:00
EthanHealy01 9fd8fd89ed add enum SERVER to list of valid licenses (#5172) 2025-12-05 13:18:23 +00:00
Keon Chen 3a2370ea1f Update OCR setup guide link in LanguagePicker (#5162)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
Update the link to the doc page for language picker in OCR config
- Why the change was made
Old link doesn't work anymore
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ x] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [x ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ x] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ x] I have performed a self-review of my own code
- [ x] My changes generate no new warnings

### Documentation

- [x ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [x ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-12-04 21:35:11 +00:00
James BruntonandConnorYoh e7db714091 More fixes for automate (#5168)
# Description of Changes
Fix file missed in #5127 to use `apiClient` instead of `axios` directly

Co-authored-by: ConnorYoh <40631091+ConnorYoh@users.noreply.github.com>
2025-12-04 17:53:08 +00:00
ConnorYohandJames Brunton c6b4a2b141 Desktop to match normal login screens (#5122)1
Also fixed issue with csrf
Also fixed issue with rust keychain

---------

Co-authored-by: James Brunton <jbrunton96@gmail.com>
2025-12-04 17:48:19 +00:00
Anthony Stirling 7459463a3c V2 sso in server plan (#5158)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-12-03 21:12:29 +00:00
EthanHealy01andAnthony Stirling c9bf436895 couple of small fixes for text editor (#5155)
# Description of Changes

- Workbench.tsx: Allow PDF text editor to handle it's own custom state
(this is a bit of a hotfix, we're going to handle the PDF text editors
file upload flow better in a future PR).
- PdfTextEditorView.tsx: Added a dropzone and some helper text to upload
the first file.
- PdfTextEditorView.tsx: Hide document view when isConverting is true.
Prevents showing stale content from previous file during conversion.
- useTranslatedToolRegistry.tsx: Moved PDF Text Editor to top of
Recommended tools list. Increased visibility of the new feature.
- PdfTextEditor.tsx: Removed auto-navigation to PDF Editor workbench on
file selection. Stops the "jumpy" behavior when selecting files.
- HomePage.tsx: Check specifically for pdfTextEditor tool instead of any
custom workbench. Prevents auto-switch to viewer when uploading files
while in PDF Text Editor.

<img width="2056" height="1073" alt="Screenshot 2025-12-03 at 6 01
14 PM"
src="https://github.com/user-attachments/assets/dfc63a46-7991-486c-ba00-0ce7637502f5"
/>


---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2025-12-03 20:37:23 +00:00
EthanHealy01 f8dbf171e1 Feature/v2/get all info on pdf (#5105)
# Description of Changes

- Addition of the get all info on PDF tool

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-12-03 20:02:42 +00:00
ConnorYoh e59c717dc0 Fixes state management loops around getting results V2 (#5153)
Makes sure settings step collapses in results step

Makes sure result step doesn't always reset even in development for
baseTool

Make sure result step doesn't reset for convert
2025-12-03 17:42:04 +00:00
ConnorYoh f2bffe2dc6 Fix-convert-V2 (#5147)
Custom processors can now return consume all inputs flag. This allows to
have many inputs to single output consumption

Fixed multi call conversion logic
2025-12-03 17:39:49 +00:00
Anthony Stirling 5d827df08c Add onboarding bypass flag V2 version2 version 2 (#5151)
## Summary
- add a shared hook that honors a `bypassOnboarding` query parameter and
marks onboarding steps as completed for the session
- block onboarding orchestrator and UI elements when the bypass flag is
present so tours and popups stay hidden

## Testing
- ./gradlew build


------
[Codex
Task](https://chatgpt.com/codex/tasks/task_b_693059f866a8832891dd97f3d52ca5a0)
2025-12-03 17:17:22 +00:00
Anthony Stirling bdb3c887f3 opensource text editor (#5146)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-12-03 12:55:34 +00:00
f902e8aca9 OAuth Provider Buttons (#5103)
PR to allow other OAuth providers to be displayed on the login screen.
Will show a generic icon when the provider is not in the known set of
providers.
<img width="424" height="205" alt="47ab288dadbc889fd84cc83c9ded0829"
src="https://github.com/user-attachments/assets/2877eb3d-2ade-406f-a2bf-dc404793e30f"
/>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: stirlingbot[bot] <stirlingbot[bot]@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ludy <Ludy87@users.noreply.github.com>
Co-authored-by: EthanHealy01 <80844253+EthanHealy01@users.noreply.github.com>
Co-authored-by: Ethan <ethan@MacBook-Pro.local>
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com>
2025-12-03 10:54:53 +00:00
Anthony Stirling 65a3eeca76 Toml (#5115)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-12-03 09:57:00 +00:00
Anthony Stirling f72538d30f Add files via upload 2025-12-03 09:11:05 +00:00
Anthony Stirling 88c5fb46ad Bump version from 2.0.2 to 2.0.3 2025-12-02 23:39:27 +00:00
Anthony Stirling 8e2f9546a5 fixes for automate (#5127)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-12-02 23:38:19 +00:00
f2f4bd5230 Bug/v2/signature fixes (#5104)
Co-authored-by: Dario Ghunney Ware <dariogware@gmail.com>
Co-authored-by: James Brunton <jbrunton96@gmail.com>
2025-12-02 22:48:29 +00:00
Anthony Stirling f3cc30d0c2 Update wording for third-party services reference 2025-12-02 19:14:03 +00:00
Reece Browne ba7c75aff4 Update embed and allow form rendering (#5124)
You can now see the values of forms filled elsewhre (still no filling
forms ourselves)
2025-12-02 18:56:22 +00:00
Anthony Stirling a53d73ef51 Revise README for improved structure and clarity (#5121)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-12-02 17:25:59 +00:00
c2a63cf425 java frontend (#5097)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Co-authored-by: Reece <reece@stirlingpdf.com>
Co-authored-by: Reece Browne <74901996+reecebrowne@users.noreply.github.com>
2025-12-02 17:15:29 +00:00
Reece Browne c3456adc2b Print with embed (#5109) 2025-12-02 13:56:28 +00:00
EthanHealy01 179b569769 Chore/v2/onboarding flow cleanup (#5065) 2025-12-02 12:40:20 +00:00
341adaa07d Grandpa Fix (#5030)
PR to address inactive accounts (invited/pending activation) not being
grandfathered during migration

---------

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: stirlingbot[bot] <stirlingbot[bot]@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ludy <Ludy87@users.noreply.github.com>
Co-authored-by: EthanHealy01 <80844253+EthanHealy01@users.noreply.github.com>
Co-authored-by: Ethan <ethan@MacBook-Pro.local>
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com>
2025-12-02 12:34:38 +00:00
feebfe82fa Reduce JWT Logs (#5108)
Removed logging in some areas and changed level from `WARN` -> `DEBUG`
to reduce verbosity

Closes #5089

---------

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: stirlingbot[bot] <stirlingbot[bot]@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ludy <Ludy87@users.noreply.github.com>
Co-authored-by: EthanHealy01 <80844253+EthanHealy01@users.noreply.github.com>
Co-authored-by: Ethan <ethan@MacBook-Pro.local>
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com>
2025-12-02 12:34:17 +00:00
ConnorYohandCopilot Autofix powered by AI 1e72416d55 Added file endpoint for license files and easy upload in admin UI (#5055)
<img width="698" height="240" alt="image"
src="https://github.com/user-attachments/assets/f0161e5f-e2ed-44c1-bdd1-93fab46f756b"
/>

---------

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2025-11-29 19:35:50 +00:00
Anthony Stirling 959d14f075 Bump version from 2.0.1 to 2.0.2 2025-11-29 19:29:58 +00:00
Reece BrowneandClaude 8f6fcee428 Fix: Only block export when signatures are placed but not applied (#5084)
Previously, activating signature placement mode would immediately set
signaturesApplied=false, blocking export even when no signatures were
actually placed on the PDF. This caused the "unapplied signatures"
warning to appear incorrectly after clicking "Apply Signatures".

Changes:
- Remove signaturesApplied=false from activateDrawMode and
activateSignaturePlacementMode in SignatureContext
- Add signaturesApplied=false to onAnnotationEvent handler in
SignatureAPIBridge when event.type === 'create'
- Now signatures are only marked as unapplied when actually placed

This ensures:
- Users can activate placement mode without triggering export warning
- Export is only blocked when signatures are actually placed but not
applied
- After applying signatures, users can immediately export without
warning

🤖 Generated with [Claude Code](https://claude.com/claude-code)

# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

Co-authored-by: Claude <noreply@anthropic.com>
2025-11-29 19:29:30 +00:00
Reece BrowneandAnthony Stirling 651f17f1c6 Save signatures to server (#5080)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2025-11-29 19:29:06 +00:00
Anthony Stirling fde449e738 Use frontend translations for OCR language picker (#5051)
## Summary
- revert OCR ui-data endpoint to only expose language codes without
backend labels
- translate OCR language options on the frontend using existing lang.*
keys with locale-aware fallbacks

## Testing
- Not run (not requested)


------
[Codex
Task](https://chatgpt.com/codex/tasks/task_b_6928ae7c85448328a0d2660a0c021b22)
2025-11-29 16:05:15 +00:00
Anthony Stirling 85e9121745 desktop fix debian (#5068)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-11-29 16:04:43 +00:00
Anthony Stirling 12f1fd485e Audit viewer making api call when shouldnt (#5069)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-11-29 16:04:29 +00:00
Anthony Stirling b49e8a2355 tauri remote connection fix (#5070)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-11-29 16:04:19 +00:00
Anthony StirlingandReece Browne d908bc6785 Swagger fixes (#5071)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

Co-authored-by: Reece Browne <74901996+reecebrowne@users.noreply.github.com>
2025-11-29 16:03:57 +00:00
Anthony Stirling 4ae79d92ae Fix email invite/ allow non auth and table refresh issues (#5076)
# Description of Changes

- Show warning when email invite fails but user is created
  - Auto-refresh user/team tables after modifications
  - Fix invite email URLs to use frontend URL instead of backend
  - Support anonymous SMTP for local development
---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-11-29 16:03:44 +00:00
Balázs Szücs 85d9b5b83d feat(viewer): Add interactive link layer with (basic) internal/external navigation support (#5077)
# Description of Changes

Added a new `LinkLayer` component to the PDF viewer that renders
clickable overlays for PDF link annotations, enabling both internal page
navigation and external URL links.

- Created `LinkLayer.tsx` component that extracts link annotations using
the EmbedPDF annotation API with fallback to direct PDF document access
- Implemented scale-aware positioning to maintain accurate link hotspots
at different zoom levels
- Added support for internal navigation (GoTo actions) using smooth
scrolling and external links (URI actions) opening in new tabs
- Integrated accessibility features with proper ARIA labels and keyboard
navigation
- Modified `LocalEmbedPDF.tsx` to always register the annotation plugin
(even when editing is disabled) to enable reading existing link
annotations
- Updated `ReactRoutingController.java` and test formatting for code
style consistency


**Key features:**
- Multi-source annotation detection (annotation API → document API →
page API fallback)
- Navigation lock to prevent race conditions
- React performance optimizations (useMemo, useCallback)
- TypeScript type safety for PDF actions and destinations



This does not address support for Attachment links. Sadly, that does not
seem to be possible with EmbedPDF


<img width="773" height="957" alt="image"
src="https://github.com/user-attachments/assets/8a04d15a-79b5-46b6-af8b-3d27246581a7"
/>
<img width="773" height="957" alt="image"
src="https://github.com/user-attachments/assets/eeb39ca7-a114-4bd5-a4eb-9e8a27331297"
/>
<img width="773" height="957" alt="image"
src="https://github.com/user-attachments/assets/1ee6bba3-d233-4a11-bf1a-1b56696265e1"
/>


<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [X] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [X] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [X] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

Signed-off-by: Balázs Szücs <bszucs1209@gmail.com>
2025-11-29 12:53:26 +00:00
Reece Browne 058a81d554 Improved language select (#5062) 2025-11-28 22:49:23 +00:00
Reece Browne e4c6ce5836 Cookie consent banner and footer on login screen (#5042) 2025-11-28 16:24:10 +00:00
ConnorYoh 250979e271 Desktop Self-hosted guidance improvements (#5060)
Removed timeout when waiting for backend port to allow for slow backend
spinups



<img width="1185" height="951" alt="image"
src="https://github.com/user-attachments/assets/badaf8e5-611d-44aa-aca2-7c1c906c2019"
/>
<img width="1213" height="1533" alt="image"
src="https://github.com/user-attachments/assets/ce78b67a-07e0-4c23-9087-5de0c5f203c6"
/>
<img width="1207" height="1202" alt="image"
src="https://github.com/user-attachments/assets/c6e5b4c5-9cc3-4973-a634-3b7aa1e1dd34"
/>
2025-11-28 15:55:37 +00:00
James Brunton 731743b618 Don't block desktop app on backend starting up (#5041)
# Description of Changes
Start bundled backend instantly on startup of app and don't wait on it
being fully up to spawn app. This is techincally wasteful curently on
self-hosted mode where everything runs remotely, but in the future we'll
probably route simple operations to the local machine regardless of
connection, and it stops unnecessary waiting in the offline mode.
2025-11-27 15:54:35 +00:00
ConnorYoh 04c4aec0d8 Disable admin plan section when no login (#5039)
Admin plan section matches other admin sections

<img width="1013" height="629" alt="image"
src="https://github.com/user-attachments/assets/39e9fad7-461c-491d-99cb-4b140292f2f4"
/>
<img width="730" height="595" alt="image"
src="https://github.com/user-attachments/assets/b26354d2-5401-40b3-8ca7-3b48b26b644e"
/>
2025-11-27 13:57:04 +00:00
Anthony Stirling f63df148ad Create LICENSE 2025-11-27 11:14:15 +00:00
Anthony Stirling 6f0be94bd6 Update LICENSE with proprietary directory details
Added licensing information for proprietary directories.
2025-11-27 11:11:53 +00:00
Anthony Stirling e47ed13be8 Builds custom Jar (#5029)
# Description of Changes

Change jar files to contain frontend if provided with param, else
doesnt... add release artifact -server version which wont have frontend

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-11-26 17:21:42 +00:00
Reece Browne a62c8b54cf Chore/v2/hide banner in onboard (#5032)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-11-26 17:19:48 +00:00
Reece Browne 1e34038b1e various cookie banner fixes (#5027)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-11-26 15:57:04 +00:00
Anthony Stirling d4765938a8 publish GHAs (#5026)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-11-26 15:22:02 +00:00
Anthony Stirling 476e1505ee Merge pull request #4826 from Stirling-Tools/V2-translations
Recreate translation files
2025-11-26 15:04:15 +00:00
James Brunton d3491ecccf Remove JSON translation files 2025-11-26 14:13:54 +00:00
James Brunton cddd5e7d15 Convert V2 translations to Toml 2025-11-26 14:13:54 +00:00
James Brunton f4c9becce2 Move translation files to V2 2025-11-26 14:06:52 +00:00
James Brunton 7629d89356 Fix login loop on self-hosted desktop (#5022)
# Description of Changes
Fix #5017 

Changes the handling of jwt tokens to be stored in local storage as well
as OS keyring so the rest of the app knows that you're logged in.
2025-11-26 13:51:12 +00:00
ConnorYoh 9fdb5295cb Added posthog variables directly to code (#5024) 2025-11-26 12:32:02 +00:00
EthanHealy01 21657a3fa6 fix the text color for the first time setup with default credentials (#5023)
# Description of Changes

Fix for this:
<img width="584" height="203" alt="Screenshot 2025-11-26 at 11 58 17 AM"
src="https://github.com/user-attachments/assets/fb5ea4ae-e42a-407a-a890-933d9f6db829"
/>

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-11-26 12:14:04 +00:00
James Brunton bc11875081 Fix CORS issues in desktop app (#5019)
# Description of Changes
In the 2.0.0 release version, the frontend can't connect to the backend
on my machine because all the network requests 403. I think this is
because of CORS issues, and supposedly these will be fixed by using a
different Spring function, which is more lenient on URL schemes (needs
to allow `tauri://localhost` here, which isn't a standard URL)
2025-11-26 10:02:38 +00:00
Reece Browne 31b3219169 Detect backend down (#5010)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-11-25 22:10:38 +00:00
d8a99fcb07 Use proper Windows APIs for checking/setting default app (#5000)
# Description of Changes
Use proper Windows APIs for checking/setting default app

---------

Co-authored-by: Connor Yoh <con.yoh13@gmail.com>
Co-authored-by: ConnorYoh <40631091+ConnorYoh@users.noreply.github.com>
2025-11-25 21:31:02 +00:00
Reece Browne 8016d271aa Handle non-pdf gracefully in viewer (#5004)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-11-25 21:27:47 +00:00
Anthony Stirling 8f1bef7f46 translationsNew (#5015)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-11-25 21:25:16 +00:00
e6db57e031 grandfather users (#4984)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Co-authored-by: ConnorYoh <40631091+ConnorYoh@users.noreply.github.com>
Co-authored-by: Dario Ghunney Ware <dariogware@gmail.com>
2025-11-25 21:23:32 +00:00
Reece Browne daf749e6be File in context indication (#4990)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-11-25 20:28:24 +00:00
Reece Browne 53d167eda5 Chore/v2/translation fixes (#5011)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-11-25 20:03:47 +00:00
d74856f675 Remove "Download for Desktop" and "Security Check" slides from desktop app onboarding (#5012)
# Description of Changes

- Title^ + I changed the text for set as default to make it more visible

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
Co-authored-by: Connor Yoh <connor@stirlingpdf.com>
2025-11-25 19:56:09 +00:00
f8386843d4 Bug/v2/onboarding slides fix (#5005)
# Description of Changes

- Stop onboarding from appearing before logging in
- Also (should've done this in a different PR) fixed a small bug in
settings when changing logo

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
Co-authored-by: Connor Yoh <connor@stirlingpdf.com>
2025-11-25 18:45:40 +00:00
ConnorYoh 2277a94c91 Added default supabase and stripe public variables (#5009)
Added to codebase
VITE_SUPABASE_URL
VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY
VITE_STRIPE_PUBLISHABLE_KEY
2025-11-25 17:58:38 +00:00
ConnorYoh 3b8b539efc Feature/v2/stripeorsupabaseNotEnabled (#5006)
Removed current plan section from static plan to match connected version

stripe publishable key not required to show plans or checkout in hosted
version

lazy load plans when needed not on load
2025-11-25 17:09:41 +00:00
Anthony Stirling 7253b9fa6d translations and login page (#5008)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-11-25 16:39:21 +00:00
EthanHealy01 9a1f89486d center login section (#5007)
# Description of Changes


<img width="2056" height="1084" alt="Screenshot 2025-11-25 at 3 54
11 PM"
src="https://github.com/user-attachments/assets/32c0b69a-4799-49f2-be8b-d75bba5e6aac"
/>


---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-11-25 16:25:58 +00:00
EthanHealy01 a6614e1bfb UI/allow logo selection (#4982)
# Description of Changes

- Allow switching between logos in-app using the same section in
settings

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-11-25 15:22:14 +00:00
Reece Browne ae5b1a4b02 consolidate english language to use gb (#5002)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-11-25 15:15:44 +00:00
ConnorYoh 9fcaace8ed Make sure license gets pushed and saved for default license replacements (#5001) 2025-11-25 14:58:33 +00:00
ConnorYoh 2ab7945130 Capture emaill when the license is default or invalid (#4998)
Stripe checkout captures email if the license is the default or invalid.
2025-11-25 14:33:19 +00:00
a8db2fda18 Feature/onboarding slides (#4955)
# Description of Changes

- Added onboarding slides/upgrade banner conditions for all the
following cases
  - 'licensed'
  - 'no-login-user-under-limit-no-license'
  - 'no-login-admin-under-limit-no-license'
  - 'no-login-user-over-limit-no-license'
  - 'no-login-admin-over-limit-no-license'
  - 'login-user-under-limit-no-license'
  - 'login-admin-under-limit-no-license'
  - 'login-user-over-limit-no-license'
  - 'login-admin-over-limit-no-license';


---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
Co-authored-by: Connor Yoh <connor@stirlingpdf.com>
2025-11-25 13:45:02 +00:00
James Brunton 80f2980755 Fix backend issues in desktop app (#4995)
# Description of Changes
Fixes two distinct but related issues in the backend of the desktop app:
- Correctly shows tools as unavaialable when the backend doesn't have
the dependencies or has disabled them etc. (same as web version - this
primarily didn't work on desktop because the app spawns before the
backend is running)
- Fixes infinite re-rendering issues caused by the app polling whether
the backend is healthy or not
2025-11-25 13:15:30 +00:00
James Brunton 2d8b0ff08c Various fixes for desktop (#4976)
# Description of Changes
- ~Force classic logo~
- Refer to email instead of username in SaaS sign in flow
- Allow drag-and-drop files into desktop app
- Convert terminology & icons from upload/download to open/save in
desktop version
2025-11-25 13:11:13 +00:00
Anthony Stirling 991e03970e Fix settings loading (#4997)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-11-25 12:30:44 +00:00
James Brunton 64d343b765 Add SSO login options to desktop app (#4954)
# Description of Changes
Add SSO login options to desktop app
2025-11-25 11:56:25 +00:00
stirlingbot[bot] 2534c532b7 🌐 [V2] Sync Translations + Update README Progress Table (#4946)
### Description of Changes

This Pull Request was automatically generated to synchronize updates to
translation files and documentation for the **V2 branch**. Below are the
details of the changes made:

#### **1. Synchronization of Translation Files**
- Updated translation files
(`frontend/public/locales/*/translation.json`) to reflect changes in the
reference file `en-GB/translation.json`.
- Ensured consistency and synchronization across all supported language
files.
- Highlighted any missing or incomplete translations.

#### **2. Update README.md**
- Generated the translation progress table in `README.md`.
- Added a summary of the current translation status for all supported
languages.
- Included up-to-date statistics on translation coverage.

#### **Why these changes are necessary**
- Keeps translation files aligned with the latest reference updates.
- Ensures the documentation reflects the current translation progress.

---

Auto-generated by [create-pull-request][1].

[1]: https://github.com/peter-evans/create-pull-request

Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com>
2025-11-25 10:01:38 +00:00
Anthony Stirling 8757a80fda Update tauri-build.yml (#4978)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-11-24 23:31:09 +00:00
James Brunton d599a07a66 Automatically switch to Viewer when going to exactly 1 file (#4973)
# Description of Changes
Automatically switch to Viewer when going to exactly 1 file
2025-11-24 17:09:41 +00:00
Reece Browne 861e4394df Bug/v2/fix rtl (#4958) 2025-11-24 16:57:36 +00:00
ConnorYohandConnor Yoh 5d18184e46 V2 Payment Features (#4974)
* Added ability to add seats to enterprise
* first logged in date on people page
* Remove Premium config section				
* Cleanup add seat flow					
* Shrink numbers in plan 					
* Make editing text a server feature in the highlights
* default to dollar pricing				
* clear checkout logic when crash				
* Recongnise location and find pricing			
* Payment successful page

---------

Co-authored-by: Connor Yoh <connor@stirlingpdf.com>
2025-11-24 16:38:07 +00:00
EthanHealy01 050408639b revert to classic logo (#4971)
# Description of Changes
 
- Revert to classic logo

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-11-24 15:04:42 +00:00
Anthony StirlingandEthanHealy01 b0397da19e PDF Text editor (#4724)
## Summary
- add a `PdfJsonConversionService` that serializes PDF text, fonts, and
metadata to JSON and rebuilds a PDF from the same structure
- expose REST endpoints for `/pdf/json` and `/json/pdf` conversions
using the existing convert API infrastructure
- define JSON model classes capturing document metadata, font
information, and positioned text elements

## Testing
- `./gradlew spotlessApply` *(fails: plugin
org.springframework.boot:3.5.4 unavailable in build environment)*
- `./gradlew build` *(fails: plugin org.springframework.boot:3.5.4
unavailable in build environment)*

------
https://chatgpt.com/codex/tasks/task_b_68f8e98d94ac8328a0e499e541528b6f

---------

Co-authored-by: EthanHealy01 <ethan.healy.21@gmail.com>
2025-11-24 14:15:02 +00:00
Reece Browne d42065e338 Feature/v2/add image (#4956)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-11-24 14:00:43 +00:00
Reece Browne 30bcc38c04 Feature/v2/add text (#4951)
Refactor sign to separate out add text and add image functions. 
Implement add text as standalone tool
2025-11-24 13:37:35 +00:00
James Brunton e8e98128d2 Allow login to SaaS for desktop instead of offline mode (#4941)
# Description of Changes
Makes the desktop options to sign in with your Stirling account, or sign
into self-hosted:

<img width="608" height="456" alt="image"
src="https://github.com/user-attachments/assets/a49988ab-db3f-4333-b242-790aee5c07c6"
/>

The first option still runs everything locally, just enforces that
you've signed in for now. Future work will enable sending operations
that can't be run locally to the server.
2025-11-22 00:38:59 +00:00
Anthony Stirling e1a879a5f6 Differentiate unavailable tools by reason (#4916)
## Summary
- track endpoint disable reasons server-side and expose them through a
new `/api/v1/config/endpoints-availability` API that the frontend can
consume
- refresh the web UI tool management logic to cache endpoint details,
compute per-tool availability metadata, and show reason-specific
messaging (admin disabled vs missing dependency) when a tool cannot be
launched
- add the missing en-GB translations for the new unavailability labels
so the UI copy reflects the new distinction
<img width="1156" height="152" alt="image"
src="https://github.com/user-attachments/assets/b54eda37-fe5c-42f9-bd5f-9ee00398d1ae"
/>


<img width="930" height="168" alt="image"
src="https://github.com/user-attachments/assets/47c07ffa-adb7-4ce3-910c-b6ff73f6f993"
/>


## Testing
- `npm run typecheck:core` *(fails:
frontend/src/core/components/shared/LocalIcon.tsx expects
../../../assets/material-symbols-icons.json, which is not present in
this environment)*

------
[Codex
Task](https://chatgpt.com/codex/tasks/task_b_6919af7a493c8328bb5ac3d07e65452b)
2025-11-21 13:19:53 +00:00
Reece Browne 4fd336c26c Feature/v2/bookmarks (#4949)
bookmark panel on viewer
2025-11-21 11:33:49 +00:00
Dario Ghunney Ware fca8470637 Moar Login Fixes (#4948) 2025-11-20 20:51:53 +00:00
Balázs Szücs 76f2fd3b76 [V2] refactor(tooltips): add merge tooltips header for consistency, and update other tooltips (#4895)
# Description of Changes

Note on tooltips: I'll do more PRs on tooltips, however this has
priority, hence I submitted this for now.


#### Before:

<img width="758" height="721" alt="image"
src="https://github.com/user-attachments/assets/ce3166d2-c681-447e-82df-68fe6d18669d"
/>

<img width="782" height="485" alt="image"
src="https://github.com/user-attachments/assets/48970a4c-9911-4ade-9346-c33c5a6a319f"
/>

<img width="782" height="602" alt="image"
src="https://github.com/user-attachments/assets/9f77648d-4d1a-4e4b-93af-eaab6db849ff"
/>

<img width="782" height="602" alt="image"
src="https://github.com/user-attachments/assets/ae653534-bf04-47a0-8aab-0bb5b9c3ecef"
/>

#### After:
<img width="758" height="721" alt="image"
src="https://github.com/user-attachments/assets/8d45e876-a0df-4fe3-89a8-5184492db204"
/>

<img width="782" height="602" alt="image"
src="https://github.com/user-attachments/assets/550ee105-1286-4072-9f55-46255b3fbe80"
/>

<img width="782" height="602" alt="image"
src="https://github.com/user-attachments/assets/fba7ea93-73fe-4529-84fe-c37816375a9e"
/>
<img width="782" height="602" alt="image"
src="https://github.com/user-attachments/assets/ca452e77-b58b-4a5b-8e97-e6ccef4cf500"
/>


<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [X] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [X] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [X] I have performed a self-review of my own code
- [X] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [X] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Signed-off-by: Balázs Szücs <bszucs1209@gmail.com>
2025-11-20 15:00:16 +00:00
Balázs Szücs 06af6be14b [V2] feat(pipeline): add pre-publish sanitization workflow (#4910)
# Description of Changes
TLDR
- Created `Pre-publish-sanitization.json` default pipeline configuration
- Added sanitization operations removing metadata, JavaScript, embedded
files, and annotations
- Registered new pipeline in `GeneralUtils`
- Included "Pre-publish Sanitization" in the suggested automations list

This pull request introduces a new "Pre-publish Sanitization" workflow
for PDF files, designed to help users remove sensitive metadata and
content before publishing documents online. The changes include backend
and frontend updates to support this workflow, as well as a minor bug
fix in form data handling.

**New Pre-publish Sanitization Workflow:**

* Added a new default configuration file `Pre-publish-sanitization.json`
that defines a pipeline for sanitizing PDFs by removing JavaScript,
embedded files, metadata, annotations, flattening forms, and compressing
the document.
* Registered the new `Pre-publish-sanitization.json` config in the set
of default web UI configurations in `GeneralUtils.java`, making it
available in the application.

**Frontend Integration:**

* Added a new suggested automation called "Pre-publish Sanitization" in
the `useSuggestedAutomations` hook, including its name, description,
operations, and a new privacy icon for better UI representation.

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [X] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [X] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [X] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [X] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Signed-off-by: Balázs Szücs <bszucs1209@gmail.com>
2025-11-20 14:59:13 +00:00
Anthony Stirling c87da6d5cc Add automatic unlock prompt for encrypted PDFs (#4912)
## Summary
- propagate an `isEncrypted` flag from thumbnail generation into
processed file metadata so uploads know when a password is still present
- add queueing logic inside `FileContext` that detects encrypted
uploads, prompts the user via a new modal, and automatically runs the
Remove Password endpoint to replace the file and preserve history
- introduce a dedicated `EncryptedPdfUnlockModal` component that mirrors
existing styling and messaging for unlocking PDFs

## Testing
- npm run typecheck:core


------
[Codex
Task](https://chatgpt.com/codex/tasks/task_b_6919a0a418bc8328b886ec76a28170b7)
2025-11-20 14:54:41 +00:00
6c8d2c89fe Add initial Windows signing infrastructure (#4945)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Co-authored-by: James Brunton <james@stirlingpdf.com>
Co-authored-by: James Brunton <jbrunton96@gmail.com>
2025-11-20 12:21:42 +00:00
8d9e70c796 Stripe and license payment integration (#4935)
selfhosted stripe payment and license integration

---------

Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
Co-authored-by: Connor Yoh <connor@stirlingpdf.com>
2025-11-20 12:07:37 +00:00
James Brunton f4725b98b0 Allow desktop app to connect to selfhosted servers (#4902)
# Description of Changes
Changes the desktop app to allow connections to self-hosted servers on
first startup. This was quite involved and hit loads of CORS issues all
through the stack, but I think it's working now. This also changes the
bundled backend to spawn on an OS-decided port rather than always
spawning on `8080`, which means that the user can have other things
running on port `8080` now and the app will still work fine. There were
quite a few places that needed to be updated to decouple the app from
explicitly using `8080` and I was originally going to split those
changes out into another PR (#4939), but I couldn't get it working
independently in the time I had, so the diff here is just going to be
complex and contian two distinct changes - sorry 🙁
2025-11-20 10:03:34 +00:00
stirlingbot[bot] 75414b89f9 🌐 [V2] Sync Translations + Update README Progress Table (#4900)
### Description of Changes

This Pull Request was automatically generated to synchronize updates to
translation files and documentation for the **V2 branch**. Below are the
details of the changes made:

#### **1. Synchronization of Translation Files**
- Updated translation files
(`frontend/public/locales/*/translation.json`) to reflect changes in the
reference file `en-GB/translation.json`.
- Ensured consistency and synchronization across all supported language
files.
- Highlighted any missing or incomplete translations.

#### **2. Update README.md**
- Generated the translation progress table in `README.md`.
- Added a summary of the current translation status for all supported
languages.
- Included up-to-date statistics on translation coverage.

#### **Why these changes are necessary**
- Keeps translation files aligned with the latest reference updates.
- Ensures the documentation reflects the current translation progress.

---

Auto-generated by [create-pull-request][1].

[1]: https://github.com/peter-evans/create-pull-request

Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com>
2025-11-19 21:18:01 +00:00
Reece Browne dd1c653301 Feature/v2/saved signatures (#4899)
Add Saved signature functionality
2025-11-19 17:30:01 +00:00
Reece Browne 0f73a1cf13 Feature/sign placement UI (#4891)
Show signature preview on hover
Place signature smaller (mkatch preview size)
Retain signature in modal on close/open
Clean up ui generally
Small bug fixes
2025-11-19 12:53:25 +00:00
Anthony Stirling 8ffe53536d activeUsers (#4936)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-11-19 12:35:35 +00:00
Anthony Stirling 6177ccd333 update notif (#4937)
# Description of Changes
<img width="1521" height="1041" alt="image"
src="https://github.com/user-attachments/assets/2644bf70-0a9b-4c91-9046-02f555314608"
/>

<img width="1162" height="1598" alt="image"
src="https://github.com/user-attachments/assets/36d693f7-6fdd-4f2b-9db1-39ac336d9055"
/>

<img width="1220" height="1625" alt="image"
src="https://github.com/user-attachments/assets/4d4c19ea-0020-45fb-b15a-9f6ad377856c"
/>

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-11-19 11:54:33 +00:00
1358 changed files with 367244 additions and 312390 deletions
+1
View File
@@ -5,6 +5,7 @@ frontend/dist
frontend/build
frontend/.vite
frontend/.tauri
frontend/src-tauri/target
# Gradle build artifacts
.gradle
+17 -13
View File
@@ -6,22 +6,27 @@ app: &app
- app/(common|core|proprietary)/src/main/java/**
openapi: &openapi
- build.gradle
- app/(common|core|proprietary)/build.gradle
- app/(common|core|proprietary)/src/main/java/**
- *build
- *app
project: &project
- app/(common|core|proprietary)/src/(main|test)/java/**
- app/(common|core|proprietary)/build.gradle
- 'app/(common|core|proprietary)/src/(main|test)/resources/**/!(messages_*.properties|*.md)*'
- exampleYmlFiles/**
- gradle/**
- libs/**
- testing/**
- build.gradle
docker: &docker
- Dockerfile
- Dockerfile.fat
- Dockerfile.ultra-lite
- ".github/workflows/build.yml"
- scripts/init.sh
- scripts/init-without-ocr.sh
- exampleYmlFiles/**
project: &project
- app/(common|core|proprietary)/src/(main|test)/java/**
- *build
- "app/(common|core|proprietary)/src/(main|test)/resources/**/!(messages_*.properties|*.md)*"
- exampleYmlFiles/**
- gradle/**
- libs/**
- "testing/**/!(requirements*.txt|requirements*.in)*"
- *docker
- gradle.properties
- gradlew
- gradlew.bat
@@ -29,7 +34,6 @@ project: &project
- settings.gradle
- frontend/**
- docker/**
- testing/**
frontend: &frontend
- frontend/**
+1 -1
View File
@@ -1 +1 @@
allow-ghsas: GHSA-wrw7-89jp-8q8g
allow-ghsas: GHSA-wrw7-89jp-8q8g
+5
View File
@@ -1,4 +1,9 @@
{
"label_changer": [
"Frooodle",
"Ludy87",
"balazs-szucs"
],
"repo_devs": [
"Frooodle",
"sf298",
+3
View File
@@ -184,3 +184,6 @@
- name: "codex"
color: "ededed"
description: "chatgpt AI generated code"
- name: "break-change"
color: "FF0000"
description: "This PR introduces a breaking API change."
+4
View File
@@ -27,6 +27,10 @@ Closes #(issue_number)
- [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed)
- [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only)
### Translations (if applicable)
- [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)
### UI Changes (if applicable)
- [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR)
+4
View File
@@ -1,5 +1,9 @@
changelog:
categories:
- title: Breaking Changes
labels:
- break-change
- title: Bug Fixes
labels:
- Bug
@@ -1,403 +0,0 @@
"""
Author: Ludy87
Description: This script processes .properties files for localization checks. It compares translation files in a branch with
a reference file to ensure consistency. The script performs two main checks:
1. Verifies that the number of lines (including comments and empty lines) in the translation files matches the reference file.
2. Ensures that all keys in the translation files are present in the reference file and vice versa.
The script also provides functionality to update the translation files to match the reference file by adding missing keys and
adjusting the format.
Usage:
python check_language_properties.py --reference-file <path_to_reference_file> --branch <branch_name> [--actor <actor_name>] [--files <list_of_changed_files>]
"""
# Sample for Windows:
# python .github/scripts/check_language_properties.py --reference-file src\main\resources\messages_en_GB.properties --branch "" --files src\main\resources\messages_de_DE.properties src\main\resources\messages_uk_UA.properties
import copy
import glob
import os
import argparse
import re
def find_duplicate_keys(file_path):
"""
Identifies duplicate keys in a .properties file.
:param file_path: Path to the .properties file.
:return: List of tuples (key, first_occurrence_line, duplicate_line).
"""
keys = {}
duplicates = []
with open(file_path, "r", encoding="utf-8") as file:
for line_number, line in enumerate(file, start=1):
stripped_line = line.strip()
# Skip empty lines and comments
if not stripped_line or stripped_line.startswith("#"):
continue
# Split the line into key and value
if "=" in stripped_line:
key, _ = stripped_line.split("=", 1)
key = key.strip()
# Check if the key already exists
if key in keys:
duplicates.append((key, keys[key], line_number))
else:
keys[key] = line_number
return duplicates
# Maximum size for properties files (e.g., 200 KB)
MAX_FILE_SIZE = 200 * 1024
def parse_properties_file(file_path):
"""
Parses a .properties file and returns a structured list of its contents.
:param file_path: Path to the .properties file.
:return: List of dictionaries representing each line in the file.
"""
properties_list = []
with open(file_path, "r", encoding="utf-8") as file:
for line_number, line in enumerate(file, start=1):
stripped_line = line.strip()
# Handle empty lines
if not stripped_line:
properties_list.append(
{"line_number": line_number, "type": "empty", "content": ""}
)
continue
# Handle comments
if stripped_line.startswith("#"):
properties_list.append(
{
"line_number": line_number,
"type": "comment",
"content": stripped_line,
}
)
continue
# Handle key-value pairs
match = re.match(r"^([^=]+)=(.*)$", line)
if match:
key, value = match.groups()
properties_list.append(
{
"line_number": line_number,
"type": "entry",
"key": key.strip(),
"value": value.strip(),
}
)
return properties_list
def write_json_file(file_path, updated_properties):
"""
Writes updated properties back to the file in their original format.
:param file_path: Path to the .properties file.
:param updated_properties: List of updated properties to write.
"""
updated_lines = {entry["line_number"]: entry for entry in updated_properties}
# Sort lines by their numbers and retain comments and empty lines
all_lines = sorted(set(updated_lines.keys()))
original_format = []
for line in all_lines:
if line in updated_lines:
entry = updated_lines[line]
else:
entry = None
ref_entry = updated_lines[line]
if ref_entry["type"] in ["comment", "empty"]:
original_format.append(ref_entry)
elif entry is None:
# Add missing entries from the reference file
original_format.append(ref_entry)
elif entry["type"] == "entry":
# Replace entries with those from the current JSON
original_format.append(entry)
# Write the updated content back to the file
with open(file_path, "w", encoding="utf-8", newline="\n") as file:
for entry in original_format:
if entry["type"] == "comment":
file.write(f"{entry['content']}\n")
elif entry["type"] == "empty":
file.write(f"{entry['content']}\n")
elif entry["type"] == "entry":
file.write(f"{entry['key']}={entry['value']}\n")
def update_missing_keys(reference_file, file_list, branch=""):
"""
Updates missing keys in the translation files based on the reference file.
:param reference_file: Path to the reference .properties file.
:param file_list: List of translation files to update.
:param branch: Branch where the files are located.
"""
reference_properties = parse_properties_file(reference_file)
for file_path in file_list:
basename_current_file = os.path.basename(os.path.join(branch, file_path))
if (
basename_current_file == os.path.basename(reference_file)
or not file_path.endswith(".properties")
or not basename_current_file.startswith("messages_")
):
continue
current_properties = parse_properties_file(os.path.join(branch, file_path))
updated_properties = []
for ref_entry in reference_properties:
ref_entry_copy = copy.deepcopy(ref_entry)
for current_entry in current_properties:
if current_entry["type"] == "entry":
if ref_entry_copy["type"] != "entry":
continue
if ref_entry_copy["key"].lower() == current_entry["key"].lower():
ref_entry_copy["value"] = current_entry["value"]
updated_properties.append(ref_entry_copy)
write_json_file(os.path.join(branch, file_path), updated_properties)
def check_for_missing_keys(reference_file, file_list, branch):
update_missing_keys(reference_file, file_list, branch)
def read_properties(file_path):
if os.path.isfile(file_path) and os.path.exists(file_path):
with open(file_path, "r", encoding="utf-8") as file:
return file.read().splitlines()
return [""]
def check_for_differences(reference_file, file_list, branch, actor):
reference_branch = reference_file.split("/")[0]
basename_reference_file = os.path.basename(reference_file)
report = []
report.append(f"#### 🔄 Reference Branch: `{reference_branch}`")
reference_lines = read_properties(reference_file)
has_differences = False
only_reference_file = True
file_arr = file_list
if len(file_list) == 1:
file_arr = file_list[0].split()
base_dir = os.path.abspath(
os.path.join(os.getcwd(), "app", "core", "src", "main", "resources")
)
for file_path in file_arr:
file_normpath = os.path.normpath(file_path)
absolute_path = os.path.abspath(file_normpath)
# Verify that file is within the expected directory
if not absolute_path.startswith(base_dir):
raise ValueError(f"Unsafe file found: {file_normpath}")
# Verify file size before processing
if os.path.getsize(os.path.join(branch, file_normpath)) > MAX_FILE_SIZE:
raise ValueError(
f"The file {file_normpath} is too large and could pose a security risk."
)
basename_current_file = os.path.basename(os.path.join(branch, file_normpath))
if (
basename_current_file == basename_reference_file
or (
# only local windows command
not file_normpath.startswith(
os.path.join(
"", "app", "core", "src", "main", "resources", "messages_"
)
)
and not file_normpath.startswith(
os.path.join(
os.getcwd(),
"app",
"core",
"src",
"main",
"resources",
"messages_",
)
)
)
or not file_normpath.endswith(".properties")
or not basename_current_file.startswith("messages_")
):
continue
only_reference_file = False
report.append(f"#### 📃 **File Check:** `{basename_current_file}`")
current_lines = read_properties(os.path.join(branch, file_path))
reference_line_count = len(reference_lines)
current_line_count = len(current_lines)
if reference_line_count != current_line_count:
report.append("")
report.append("1. **Test Status:** ❌ **_Failed_**")
report.append(" - **Issue:**")
has_differences = True
if reference_line_count > current_line_count:
report.append(
f" - **_Mismatched line count_**: {reference_line_count} (reference) vs {current_line_count} (current). Comments, empty lines, or translation strings are missing."
)
elif reference_line_count < current_line_count:
report.append(
f" - **_Too many lines_**: {reference_line_count} (reference) vs {current_line_count} (current). Please verify if there is an additional line that needs to be removed."
)
else:
report.append("1. **Test Status:** ✅ **_Passed_**")
# Check for missing or extra keys
current_keys = []
reference_keys = []
for line in current_lines:
if not line.startswith("#") and line != "" and "=" in line:
key, _ = line.split("=", 1)
current_keys.append(key)
for line in reference_lines:
if not line.startswith("#") and line != "" and "=" in line:
key, _ = line.split("=", 1)
reference_keys.append(key)
current_keys_set = set(current_keys)
reference_keys_set = set(reference_keys)
missing_keys = current_keys_set.difference(reference_keys_set)
extra_keys = reference_keys_set.difference(current_keys_set)
missing_keys_list = list(missing_keys)
extra_keys_list = list(extra_keys)
if missing_keys_list or extra_keys_list:
has_differences = True
missing_keys_str = "`, `".join(missing_keys_list)
extra_keys_str = "`, `".join(extra_keys_list)
report.append("2. **Test Status:** ❌ **_Failed_**")
report.append(" - **Issue:**")
if missing_keys_list:
spaces_keys_list = []
for key in missing_keys_list:
if " " in key:
spaces_keys_list.append(key)
if spaces_keys_list:
spaces_keys_str = "`, `".join(spaces_keys_list)
report.append(
f" - **_Keys containing unnecessary spaces_**: `{spaces_keys_str}`!"
)
report.append(
f" - **_Extra keys in `{basename_current_file}`_**: `{missing_keys_str}` that are not present in **_`{basename_reference_file}`_**."
)
if extra_keys_list:
report.append(
f" - **_Missing keys in `{basename_reference_file}`_**: `{extra_keys_str}` that are not present in **_`{basename_current_file}`_**."
)
else:
report.append("2. **Test Status:** ✅ **_Passed_**")
if find_duplicate_keys(os.path.join(branch, file_normpath)):
has_differences = True
output = "\n".join(
[
f" - `{key}`: first at line {first}, duplicate at `line {duplicate}`"
for key, first, duplicate in find_duplicate_keys(
os.path.join(branch, file_normpath)
)
]
)
report.append("3. **Test Status:** ❌ **_Failed_**")
report.append(" - **Issue:**")
report.append(" - duplicate entries were found:")
report.append(output)
else:
report.append("3. **Test Status:** ✅ **_Passed_**")
report.append("")
report.append("---")
report.append("")
if has_differences:
report.append("## ❌ Overall Check Status: **_Failed_**")
report.append("")
report.append(
f"@{actor} please check your translation if it conforms to the standard. Follow the format of [messages_en_GB.properties](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/app/core/src/main/resources/messages_en_GB.properties)"
)
else:
report.append("## ✅ Overall Check Status: **_Success_**")
report.append("")
report.append(
f"Thanks @{actor} for your help in keeping the translations up to date."
)
if not only_reference_file:
print("\n".join(report))
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Find missing keys")
parser.add_argument(
"--actor",
required=False,
help="Actor from PR.",
)
parser.add_argument(
"--reference-file",
required=True,
help="Path to the reference file.",
)
parser.add_argument(
"--branch",
type=str,
required=True,
help="Branch name.",
)
parser.add_argument(
"--check-file",
type=str,
required=False,
help="List of changed files, separated by spaces.",
)
parser.add_argument(
"--files",
nargs="+",
required=False,
help="List of changed files, separated by spaces.",
)
args = parser.parse_args()
# Sanitize --actor input to avoid injection attacks
if args.actor:
args.actor = re.sub(r"[^a-zA-Z0-9_\\-]", "", args.actor)
# Sanitize --branch input to avoid injection attacks
if args.branch:
args.branch = re.sub(r"[^a-zA-Z0-9\\-]", "", args.branch)
file_list = args.files
if file_list is None:
if args.check_file:
file_list = [args.check_file]
else:
file_list = glob.glob(
os.path.join(
os.getcwd(),
"app",
"core",
"src",
"main",
"resources",
"messages_*.properties",
)
)
update_missing_keys(args.reference_file, file_list)
else:
check_for_differences(args.reference_file, file_list, args.branch, args.actor)
@@ -1,6 +1,6 @@
"""
Author: Ludy87
Description: This script processes JSON translation files for localization checks. It compares translation files in a branch with
Description: This script processes TOML translation files for localization checks. It compares translation files in a branch with
a reference file to ensure consistency. The script performs two main checks:
1. Verifies that the number of translation keys in the translation files matches the reference file.
2. Ensures that all keys in the translation files are present in the reference file and vice versa.
@@ -9,23 +9,23 @@ The script also provides functionality to update the translation files to match
adjusting the format.
Usage:
python check_language_json.py --reference-file <path_to_reference_file> --branch <branch_name> [--actor <actor_name>] [--files <list_of_changed_files>]
python check_language_toml.py --reference-file <path_to_reference_file> --branch <branch_name> [--actor <actor_name>] [--files <list_of_changed_files>]
"""
# Sample for Windows:
# python .github/scripts/check_language_json.py --reference-file frontend/public/locales/en-GB/translation.json --branch "" --files frontend/public/locales/de-DE/translation.json frontend/public/locales/fr-FR/translation.json
# python .github/scripts/check_language_toml.py --reference-file frontend/public/locales/en-GB/translation.toml --branch "" --files frontend/public/locales/de-DE/translation.toml frontend/public/locales/fr-FR/translation.toml
import copy
import glob
import os
import argparse
import re
import json
import tomllib # Python 3.11+ (stdlib)
import tomli_w # For writing TOML files
def find_duplicate_keys(file_path, keys=None, prefix=""):
"""
Identifies duplicate keys in a JSON file (including nested keys).
:param file_path: Path to the JSON file.
Identifies duplicate keys in a TOML file (including nested keys).
:param file_path: Path to the TOML file.
:param keys: Dictionary to track keys (used for recursion).
:param prefix: Prefix for nested keys.
:return: List of tuples (key, first_occurrence_path, duplicate_path).
@@ -35,8 +35,9 @@ def find_duplicate_keys(file_path, keys=None, prefix=""):
duplicates = []
with open(file_path, "r", encoding="utf-8") as file:
data = json.load(file)
# Load TOML file
with open(file_path, "rb") as file:
data = tomllib.load(file)
def process_dict(obj, current_prefix=""):
for key, value in obj.items():
@@ -54,18 +55,18 @@ def find_duplicate_keys(file_path, keys=None, prefix=""):
return duplicates
# Maximum size for JSON files (e.g., 500 KB)
# Maximum size for TOML files (e.g., 500 KB)
MAX_FILE_SIZE = 500 * 1024
def parse_json_file(file_path):
def parse_toml_file(file_path):
"""
Parses a JSON translation file and returns a flat dictionary of all keys.
:param file_path: Path to the JSON file.
Parses a TOML translation file and returns a flat dictionary of all keys.
:param file_path: Path to the TOML file.
:return: Dictionary with flattened keys.
"""
with open(file_path, "r", encoding="utf-8") as file:
data = json.load(file)
with open(file_path, "rb") as file:
data = tomllib.load(file)
def flatten_dict(d, parent_key="", sep="."):
items = {}
@@ -99,38 +100,37 @@ def unflatten_dict(d, sep="."):
return result
def write_json_file(file_path, updated_properties):
def write_toml_file(file_path, updated_properties):
"""
Writes updated properties back to the JSON file.
:param file_path: Path to the JSON file.
Writes updated properties back to the TOML file.
:param file_path: Path to the TOML file.
:param updated_properties: Dictionary of updated properties to write.
"""
nested_data = unflatten_dict(updated_properties)
with open(file_path, "w", encoding="utf-8", newline="\n") as file:
json.dump(nested_data, file, ensure_ascii=False, indent=2)
file.write("\n") # Add trailing newline
with open(file_path, "wb") as file:
tomli_w.dump(nested_data, file)
def update_missing_keys(reference_file, file_list, branch=""):
"""
Updates missing keys in the translation files based on the reference file.
:param reference_file: Path to the reference JSON file.
:param reference_file: Path to the reference TOML file.
:param file_list: List of translation files to update.
:param branch: Branch where the files are located.
"""
reference_properties = parse_json_file(reference_file)
reference_properties = parse_toml_file(reference_file)
for file_path in file_list:
basename_current_file = os.path.basename(os.path.join(branch, file_path))
if (
basename_current_file == os.path.basename(reference_file)
or not file_path.endswith(".json")
or not file_path.endswith(".toml")
or not os.path.dirname(file_path).endswith("locales")
):
continue
current_properties = parse_json_file(os.path.join(branch, file_path))
current_properties = parse_toml_file(os.path.join(branch, file_path))
updated_properties = {}
for ref_key, ref_value in reference_properties.items():
@@ -141,16 +141,16 @@ def update_missing_keys(reference_file, file_list, branch=""):
# Add missing key with reference value
updated_properties[ref_key] = ref_value
write_json_file(os.path.join(branch, file_path), updated_properties)
write_toml_file(os.path.join(branch, file_path), updated_properties)
def check_for_missing_keys(reference_file, file_list, branch):
update_missing_keys(reference_file, file_list, branch)
def read_json_keys(file_path):
def read_toml_keys(file_path):
if os.path.isfile(file_path) and os.path.exists(file_path):
return parse_json_file(file_path)
return parse_toml_file(file_path)
return {}
@@ -160,7 +160,7 @@ def check_for_differences(reference_file, file_list, branch, actor):
report = []
report.append(f"#### 🔄 Reference Branch: `{reference_branch}`")
reference_keys = read_json_keys(reference_file)
reference_keys = read_toml_keys(reference_file)
has_differences = False
only_reference_file = True
@@ -191,18 +191,18 @@ def check_for_differences(reference_file, file_list, branch, actor):
basename_current_file = os.path.basename(os.path.join(branch, file_normpath))
locale_dir = os.path.basename(os.path.dirname(file_normpath))
if (
basename_current_file == basename_reference_file
and locale_dir == "en-GB"
):
if basename_current_file == basename_reference_file and locale_dir == "en-GB":
continue
if not file_normpath.endswith(".json") or basename_current_file != "translation.json":
if (
not file_normpath.endswith(".toml")
or basename_current_file != "translation.toml"
):
continue
only_reference_file = False
report.append(f"#### 📃 **File Check:** `{locale_dir}/{basename_current_file}`")
current_keys = read_json_keys(os.path.join(branch, file_path))
current_keys = read_toml_keys(os.path.join(branch, file_path))
reference_key_count = len(reference_keys)
current_key_count = len(current_keys)
@@ -272,7 +272,7 @@ def check_for_differences(reference_file, file_list, branch, actor):
report.append("## ❌ Overall Check Status: **_Failed_**")
report.append("")
report.append(
f"@{actor} please check your translation if it conforms to the standard. Follow the format of [en-GB/translation.json](https://github.com/Stirling-Tools/Stirling-PDF/blob/V2/frontend/public/locales/en-GB/translation.json)"
f"@{actor} please check your translation if it conforms to the standard. Follow the format of [en-GB/translation.toml](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/frontend/public/locales/en-GB/translation.toml)"
)
else:
report.append("## ✅ Overall Check Status: **_Success_**")
@@ -286,7 +286,9 @@ def check_for_differences(reference_file, file_list, branch, actor):
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Find missing keys")
parser = argparse.ArgumentParser(
description="Find missing keys in TOML translation files"
)
parser.add_argument(
"--actor",
required=False,
@@ -337,9 +339,9 @@ if __name__ == "__main__":
"public",
"locales",
"*",
"translation.json",
"translation.toml",
)
)
update_missing_keys(args.reference_file, file_list)
else:
check_for_differences(args.reference_file, file_list, args.branch, args.actor)
check_for_differences(args.reference_file, file_list, args.branch, args.actor)
+1
View File
@@ -6,3 +6,4 @@ pillow
unoserver
opencv-python-headless
pre-commit
brotli @ git+https://github.com/google/brotli.git@028fb5a23661f123017c060daa546b55cf4bde29
+338 -420
View File
@@ -4,201 +4,98 @@
#
# pip-compile --allow-unsafe --generate-hashes --output-file='.github\scripts\requirements_dev.txt' --strip-extras '.github\scripts\requirements_dev.in'
#
brotli==1.1.0 \
--hash=sha256:03d20af184290887bdea3f0f78c4f737d126c74dc2f3ccadf07e54ceca3bf208 \
--hash=sha256:0541e747cce78e24ea12d69176f6a7ddb690e62c425e01d31cc065e69ce55b48 \
--hash=sha256:069a121ac97412d1fe506da790b3e69f52254b9df4eb665cd42460c837193354 \
--hash=sha256:0737ddb3068957cf1b054899b0883830bb1fec522ec76b1098f9b6e0f02d9419 \
--hash=sha256:0b63b949ff929fbc2d6d3ce0e924c9b93c9785d877a21a1b678877ffbbc4423a \
--hash=sha256:0c6244521dda65ea562d5a69b9a26120769b7a9fb3db2fe9545935ed6735b128 \
--hash=sha256:11d00ed0a83fa22d29bc6b64ef636c4552ebafcef57154b4ddd132f5638fbd1c \
--hash=sha256:141bd4d93984070e097521ed07e2575b46f817d08f9fa42b16b9b5f27b5ac088 \
--hash=sha256:19c116e796420b0cee3da1ccec3b764ed2952ccfcc298b55a10e5610ad7885f9 \
--hash=sha256:1ab4fbee0b2d9098c74f3057b2bc055a8bd92ccf02f65944a241b4349229185a \
--hash=sha256:1ae56aca0402a0f9a3431cddda62ad71666ca9d4dc3a10a142b9dce2e3c0cda3 \
--hash=sha256:1b2c248cd517c222d89e74669a4adfa5577e06ab68771a529060cf5a156e9757 \
--hash=sha256:1e9a65b5736232e7a7f91ff3d02277f11d339bf34099a56cdab6a8b3410a02b2 \
--hash=sha256:224e57f6eac61cc449f498cc5f0e1725ba2071a3d4f48d5d9dffba42db196438 \
--hash=sha256:22fc2a8549ffe699bfba2256ab2ed0421a7b8fadff114a3d201794e45a9ff578 \
--hash=sha256:23032ae55523cc7bccb4f6a0bf368cd25ad9bcdcc1990b64a647e7bbcce9cb5b \
--hash=sha256:2333e30a5e00fe0fe55903c8832e08ee9c3b1382aacf4db26664a16528d51b4b \
--hash=sha256:2954c1c23f81c2eaf0b0717d9380bd348578a94161a65b3a2afc62c86467dd68 \
--hash=sha256:2a24c50840d89ded6c9a8fdc7b6ed3692ed4e86f1c4a4a938e1e92def92933e0 \
--hash=sha256:2de9d02f5bda03d27ede52e8cfe7b865b066fa49258cbab568720aa5be80a47d \
--hash=sha256:2feb1d960f760a575dbc5ab3b1c00504b24caaf6986e2dc2b01c09c87866a943 \
--hash=sha256:30924eb4c57903d5a7526b08ef4a584acc22ab1ffa085faceb521521d2de32dd \
--hash=sha256:316cc9b17edf613ac76b1f1f305d2a748f1b976b033b049a6ecdfd5612c70409 \
--hash=sha256:32d95b80260d79926f5fab3c41701dbb818fde1c9da590e77e571eefd14abe28 \
--hash=sha256:38025d9f30cf4634f8309c6874ef871b841eb3c347e90b0851f63d1ded5212da \
--hash=sha256:39da8adedf6942d76dc3e46653e52df937a3c4d6d18fdc94a7c29d263b1f5b50 \
--hash=sha256:3c0ef38c7a7014ffac184db9e04debe495d317cc9c6fb10071f7fefd93100a4f \
--hash=sha256:3d7954194c36e304e1523f55d7042c59dc53ec20dd4e9ea9d151f1b62b4415c0 \
--hash=sha256:3ee8a80d67a4334482d9712b8e83ca6b1d9bc7e351931252ebef5d8f7335a547 \
--hash=sha256:4093c631e96fdd49e0377a9c167bfd75b6d0bad2ace734c6eb20b348bc3ea180 \
--hash=sha256:43395e90523f9c23a3d5bdf004733246fba087f2948f87ab28015f12359ca6a0 \
--hash=sha256:43ce1b9935bfa1ede40028054d7f48b5469cd02733a365eec8a329ffd342915d \
--hash=sha256:4410f84b33374409552ac9b6903507cdb31cd30d2501fc5ca13d18f73548444a \
--hash=sha256:494994f807ba0b92092a163a0a283961369a65f6cbe01e8891132b7a320e61eb \
--hash=sha256:4d4a848d1837973bf0f4b5e54e3bec977d99be36a7895c61abb659301b02c112 \
--hash=sha256:4ed11165dd45ce798d99a136808a794a748d5dc38511303239d4e2363c0695dc \
--hash=sha256:4f3607b129417e111e30637af1b56f24f7a49e64763253bbc275c75fa887d4b2 \
--hash=sha256:510b5b1bfbe20e1a7b3baf5fed9e9451873559a976c1a78eebaa3b86c57b4265 \
--hash=sha256:524f35912131cc2cabb00edfd8d573b07f2d9f21fa824bd3fb19725a9cf06327 \
--hash=sha256:587ca6d3cef6e4e868102672d3bd9dc9698c309ba56d41c2b9c85bbb903cdb95 \
--hash=sha256:58d4b711689366d4a03ac7957ab8c28890415e267f9b6589969e74b6e42225ec \
--hash=sha256:5b3cc074004d968722f51e550b41a27be656ec48f8afaeeb45ebf65b561481dd \
--hash=sha256:5dab0844f2cf82be357a0eb11a9087f70c5430b2c241493fc122bb6f2bb0917c \
--hash=sha256:5e55da2c8724191e5b557f8e18943b1b4839b8efc3ef60d65985bcf6f587dd38 \
--hash=sha256:5eeb539606f18a0b232d4ba45adccde4125592f3f636a6182b4a8a436548b914 \
--hash=sha256:5f4d5ea15c9382135076d2fb28dde923352fe02951e66935a9efaac8f10e81b0 \
--hash=sha256:5fb2ce4b8045c78ebbc7b8f3c15062e435d47e7393cc57c25115cfd49883747a \
--hash=sha256:6172447e1b368dcbc458925e5ddaf9113477b0ed542df258d84fa28fc45ceea7 \
--hash=sha256:6967ced6730aed543b8673008b5a391c3b1076d834ca438bbd70635c73775368 \
--hash=sha256:6974f52a02321b36847cd19d1b8e381bf39939c21efd6ee2fc13a28b0d99348c \
--hash=sha256:6c3020404e0b5eefd7c9485ccf8393cfb75ec38ce75586e046573c9dc29967a0 \
--hash=sha256:6c6e0c425f22c1c719c42670d561ad682f7bfeeef918edea971a79ac5252437f \
--hash=sha256:70051525001750221daa10907c77830bc889cb6d865cc0b813d9db7fefc21451 \
--hash=sha256:7905193081db9bfa73b1219140b3d315831cbff0d8941f22da695832f0dd188f \
--hash=sha256:7bc37c4d6b87fb1017ea28c9508b36bbcb0c3d18b4260fcdf08b200c74a6aee8 \
--hash=sha256:7c4855522edb2e6ae7fdb58e07c3ba9111e7621a8956f481c68d5d979c93032e \
--hash=sha256:7e4c4629ddad63006efa0ef968c8e4751c5868ff0b1c5c40f76524e894c50248 \
--hash=sha256:7eedaa5d036d9336c95915035fb57422054014ebdeb6f3b42eac809928e40d0c \
--hash=sha256:7f4bf76817c14aa98cc6697ac02f3972cb8c3da93e9ef16b9c66573a68014f91 \
--hash=sha256:81de08ac11bcb85841e440c13611c00b67d3bf82698314928d0b676362546724 \
--hash=sha256:832436e59afb93e1836081a20f324cb185836c617659b07b129141a8426973c7 \
--hash=sha256:861bf317735688269936f755fa136a99d1ed526883859f86e41a5d43c61d8966 \
--hash=sha256:87a3044c3a35055527ac75e419dfa9f4f3667a1e887ee80360589eb8c90aabb9 \
--hash=sha256:890b5a14ce214389b2cc36ce82f3093f96f4cc730c1cffdbefff77a7c71f2a97 \
--hash=sha256:89f4988c7203739d48c6f806f1e87a1d96e0806d44f0fba61dba81392c9e474d \
--hash=sha256:8bf32b98b75c13ec7cf774164172683d6e7891088f6316e54425fde1efc276d5 \
--hash=sha256:8dadd1314583ec0bf2d1379f7008ad627cd6336625d6679cf2f8e67081b83acf \
--hash=sha256:901032ff242d479a0efa956d853d16875d42157f98951c0230f69e69f9c09bac \
--hash=sha256:9011560a466d2eb3f5a6e4929cf4a09be405c64154e12df0dd72713f6500e32b \
--hash=sha256:906bc3a79de8c4ae5b86d3d75a8b77e44404b0f4261714306e3ad248d8ab0951 \
--hash=sha256:919e32f147ae93a09fe064d77d5ebf4e35502a8df75c29fb05788528e330fe74 \
--hash=sha256:91d7cc2a76b5567591d12c01f019dd7afce6ba8cba6571187e21e2fc418ae648 \
--hash=sha256:929811df5462e182b13920da56c6e0284af407d1de637d8e536c5cd00a7daf60 \
--hash=sha256:949f3b7c29912693cee0afcf09acd6ebc04c57af949d9bf77d6101ebb61e388c \
--hash=sha256:a090ca607cbb6a34b0391776f0cb48062081f5f60ddcce5d11838e67a01928d1 \
--hash=sha256:a1fd8a29719ccce974d523580987b7f8229aeace506952fa9ce1d53a033873c8 \
--hash=sha256:a37b8f0391212d29b3a91a799c8e4a2855e0576911cdfb2515487e30e322253d \
--hash=sha256:a3daabb76a78f829cafc365531c972016e4aa8d5b4bf60660ad8ecee19df7ccc \
--hash=sha256:a469274ad18dc0e4d316eefa616d1d0c2ff9da369af19fa6f3daa4f09671fd61 \
--hash=sha256:a599669fd7c47233438a56936988a2478685e74854088ef5293802123b5b2460 \
--hash=sha256:a743e5a28af5f70f9c080380a5f908d4d21d40e8f0e0c8901604d15cfa9ba751 \
--hash=sha256:a77def80806c421b4b0af06f45d65a136e7ac0bdca3c09d9e2ea4e515367c7e9 \
--hash=sha256:a7e53012d2853a07a4a79c00643832161a910674a893d296c9f1259859a289d2 \
--hash=sha256:a93dde851926f4f2678e704fadeb39e16c35d8baebd5252c9fd94ce8ce68c4a0 \
--hash=sha256:aac0411d20e345dc0920bdec5548e438e999ff68d77564d5e9463a7ca9d3e7b1 \
--hash=sha256:ae15b066e5ad21366600ebec29a7ccbc86812ed267e4b28e860b8ca16a2bc474 \
--hash=sha256:aea440a510e14e818e67bfc4027880e2fb500c2ccb20ab21c7a7c8b5b4703d75 \
--hash=sha256:af6fa6817889314555aede9a919612b23739395ce767fe7fcbea9a80bf140fe5 \
--hash=sha256:b760c65308ff1e462f65d69c12e4ae085cff3b332d894637f6273a12a482d09f \
--hash=sha256:be36e3d172dc816333f33520154d708a2657ea63762ec16b62ece02ab5e4daf2 \
--hash=sha256:c247dd99d39e0338a604f8c2b3bc7061d5c2e9e2ac7ba9cc1be5a69cb6cd832f \
--hash=sha256:c5529b34c1c9d937168297f2c1fde7ebe9ebdd5e121297ff9c043bdb2ae3d6fb \
--hash=sha256:c8146669223164fc87a7e3de9f81e9423c67a79d6b3447994dfb9c95da16e2d6 \
--hash=sha256:c8fd5270e906eef71d4a8d19b7c6a43760c6abcfcc10c9101d14eb2357418de9 \
--hash=sha256:ca63e1890ede90b2e4454f9a65135a4d387a4585ff8282bb72964fab893f2111 \
--hash=sha256:caf9ee9a5775f3111642d33b86237b05808dafcd6268faa492250e9b78046eb2 \
--hash=sha256:cb1dac1770878ade83f2ccdf7d25e494f05c9165f5246b46a621cc849341dc01 \
--hash=sha256:cdad5b9014d83ca68c25d2e9444e28e967ef16e80f6b436918c700c117a85467 \
--hash=sha256:cdbc1fc1bc0bff1cef838eafe581b55bfbffaed4ed0318b724d0b71d4d377619 \
--hash=sha256:ceb64bbc6eac5a140ca649003756940f8d6a7c444a68af170b3187623b43bebf \
--hash=sha256:d0c5516f0aed654134a2fc936325cc2e642f8a0e096d075209672eb321cff408 \
--hash=sha256:d143fd47fad1db3d7c27a1b1d66162e855b5d50a89666af46e1679c496e8e579 \
--hash=sha256:d192f0f30804e55db0d0e0a35d83a9fead0e9a359a9ed0285dbacea60cc10a84 \
--hash=sha256:d2b35ca2c7f81d173d2fadc2f4f31e88cc5f7a39ae5b6db5513cf3383b0e0ec7 \
--hash=sha256:d342778ef319e1026af243ed0a07c97acf3bad33b9f29e7ae6a1f68fd083e90c \
--hash=sha256:d487f5432bf35b60ed625d7e1b448e2dc855422e87469e3f450aa5552b0eb284 \
--hash=sha256:d7702622a8b40c49bffb46e1e3ba2e81268d5c04a34f460978c6b5517a34dd52 \
--hash=sha256:db85ecf4e609a48f4b29055f1e144231b90edc90af7481aa731ba2d059226b1b \
--hash=sha256:de6551e370ef19f8de1807d0a9aa2cdfdce2e85ce88b122fe9f6b2b076837e59 \
--hash=sha256:e1140c64812cb9b06c922e77f1c26a75ec5e3f0fb2bf92cc8c58720dec276752 \
--hash=sha256:e4fe605b917c70283db7dfe5ada75e04561479075761a0b3866c081d035b01c1 \
--hash=sha256:e6a904cb26bfefc2f0a6f240bdf5233be78cd2488900a2f846f3c3ac8489ab80 \
--hash=sha256:e79e6520141d792237c70bcd7a3b122d00f2613769ae0cb61c52e89fd3443839 \
--hash=sha256:e84799f09591700a4154154cab9787452925578841a94321d5ee8fb9a9a328f0 \
--hash=sha256:e93dfc1a1165e385cc8239fab7c036fb2cd8093728cbd85097b284d7b99249a2 \
--hash=sha256:efa8b278894b14d6da122a72fefcebc28445f2d3f880ac59d46c90f4c13be9a3 \
--hash=sha256:f0d8a7a6b5983c2496e364b969f0e526647a06b075d034f3297dc66f3b360c64 \
--hash=sha256:f0db75f47be8b8abc8d9e31bc7aad0547ca26f24a54e6fd10231d623f183d089 \
--hash=sha256:f296c40e23065d0d6650c4aefe7470d2a25fffda489bcc3eb66083f3ac9f6643 \
--hash=sha256:f31859074d57b4639318523d6ffdca586ace54271a73ad23ad021acd807eb14b \
--hash=sha256:f66b5337fa213f1da0d9000bc8dc0cb5b896b726eefd9c6046f699b169c41b9e \
--hash=sha256:f733d788519c7e3e71f0855c96618720f5d3d60c3cb829d8bbb722dddce37985 \
--hash=sha256:fce1473f3ccc4187f75b4690cfc922628aed4d3dd013d047f95a9b3919a86596 \
--hash=sha256:fd5f17ff8f14003595ab414e45fce13d073e0762394f957182e69035c9f3d7c2 \
--hash=sha256:fdc3ff3bfccdc6b9cc7c342c03aa2400683f0cb891d46e94b64a197910dc4064
# via fonttools
cffi==1.17.1 \
--hash=sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8 \
--hash=sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2 \
--hash=sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1 \
--hash=sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15 \
--hash=sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36 \
--hash=sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824 \
--hash=sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8 \
--hash=sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36 \
--hash=sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17 \
--hash=sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf \
--hash=sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc \
--hash=sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3 \
--hash=sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed \
--hash=sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702 \
--hash=sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1 \
--hash=sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8 \
--hash=sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903 \
--hash=sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6 \
--hash=sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d \
--hash=sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b \
--hash=sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e \
--hash=sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be \
--hash=sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c \
--hash=sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683 \
--hash=sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9 \
--hash=sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c \
--hash=sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8 \
--hash=sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1 \
--hash=sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4 \
--hash=sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655 \
--hash=sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67 \
--hash=sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595 \
--hash=sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0 \
--hash=sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65 \
--hash=sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41 \
--hash=sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6 \
--hash=sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401 \
--hash=sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6 \
--hash=sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3 \
--hash=sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16 \
--hash=sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93 \
--hash=sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e \
--hash=sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4 \
--hash=sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964 \
--hash=sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c \
--hash=sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576 \
--hash=sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0 \
--hash=sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3 \
--hash=sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662 \
--hash=sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3 \
--hash=sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff \
--hash=sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5 \
--hash=sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd \
--hash=sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f \
--hash=sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5 \
--hash=sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14 \
--hash=sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d \
--hash=sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9 \
--hash=sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7 \
--hash=sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382 \
--hash=sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a \
--hash=sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e \
--hash=sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a \
--hash=sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4 \
--hash=sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99 \
--hash=sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87 \
--hash=sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b
# WARNING: pip install will require the following package to be hashed.
# Consider using a hashable URL like https://github.com/jazzband/pip-tools/archive/SOMECOMMIT.zip
# CVE-2025-6176 mitigation: pin brotli to a specific commit
brotli @ git+https://github.com/google/brotli.git@028fb5a23661f123017c060daa546b55cf4bde29
# via
# -r .github/scripts/requirements_dev.in
# fonttools
cffi==2.0.0 \
--hash=sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb \
--hash=sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b \
--hash=sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f \
--hash=sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9 \
--hash=sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44 \
--hash=sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2 \
--hash=sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c \
--hash=sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75 \
--hash=sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65 \
--hash=sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e \
--hash=sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a \
--hash=sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e \
--hash=sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25 \
--hash=sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a \
--hash=sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe \
--hash=sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b \
--hash=sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91 \
--hash=sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592 \
--hash=sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187 \
--hash=sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c \
--hash=sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1 \
--hash=sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94 \
--hash=sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba \
--hash=sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb \
--hash=sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165 \
--hash=sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529 \
--hash=sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca \
--hash=sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c \
--hash=sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6 \
--hash=sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c \
--hash=sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0 \
--hash=sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743 \
--hash=sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63 \
--hash=sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5 \
--hash=sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5 \
--hash=sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4 \
--hash=sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d \
--hash=sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b \
--hash=sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93 \
--hash=sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205 \
--hash=sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27 \
--hash=sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512 \
--hash=sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d \
--hash=sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c \
--hash=sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037 \
--hash=sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26 \
--hash=sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322 \
--hash=sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb \
--hash=sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c \
--hash=sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8 \
--hash=sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4 \
--hash=sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414 \
--hash=sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9 \
--hash=sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664 \
--hash=sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9 \
--hash=sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775 \
--hash=sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739 \
--hash=sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc \
--hash=sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062 \
--hash=sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe \
--hash=sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9 \
--hash=sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92 \
--hash=sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5 \
--hash=sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13 \
--hash=sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d \
--hash=sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26 \
--hash=sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f \
--hash=sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495 \
--hash=sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b \
--hash=sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6 \
--hash=sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c \
--hash=sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef \
--hash=sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5 \
--hash=sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18 \
--hash=sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad \
--hash=sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3 \
--hash=sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7 \
--hash=sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5 \
--hash=sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534 \
--hash=sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49 \
--hash=sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2 \
--hash=sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5 \
--hash=sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453 \
--hash=sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf
# via weasyprint
cfgv==3.4.0 \
--hash=sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9 \
@@ -212,57 +109,73 @@ distlib==0.4.0 \
--hash=sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16 \
--hash=sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d
# via virtualenv
filelock==3.18.0 \
--hash=sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2 \
--hash=sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de
filelock==3.20.0 \
--hash=sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2 \
--hash=sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4
# via virtualenv
fonttools==4.59.0 \
--hash=sha256:052444a5d0151878e87e3e512a1aa1a0ab35ee4c28afde0a778e23b0ace4a7de \
--hash=sha256:169b99a2553a227f7b5fea8d9ecd673aa258617f466b2abc6091fe4512a0dcd0 \
--hash=sha256:209b75943d158f610b78320eacb5539aa9e920bee2c775445b2846c65d20e19d \
--hash=sha256:21e606b2d38fed938dde871c5736822dd6bda7a4631b92e509a1f5cd1b90c5df \
--hash=sha256:241313683afd3baacb32a6bd124d0bce7404bc5280e12e291bae1b9bba28711d \
--hash=sha256:26731739daa23b872643f0e4072d5939960237d540c35c14e6a06d47d71ca8fe \
--hash=sha256:2e7cf8044ce2598bb87e44ba1d2c6e45d7a8decf56055b92906dc53f67c76d64 \
--hash=sha256:31003b6a10f70742a63126b80863ab48175fb8272a18ca0846c0482968f0588e \
--hash=sha256:332bfe685d1ac58ca8d62b8d6c71c2e52a6c64bc218dc8f7825c9ea51385aa01 \
--hash=sha256:37c377f7cb2ab2eca8a0b319c68146d34a339792f9420fca6cd49cf28d370705 \
--hash=sha256:37e01c6ec0c98599778c2e688350d624fa4770fbd6144551bd5e032f1199171c \
--hash=sha256:401b1941ce37e78b8fd119b419b617277c65ae9417742a63282257434fd68ea2 \
--hash=sha256:4536f2695fe5c1ffb528d84a35a7d3967e5558d2af58b4775e7ab1449d65767b \
--hash=sha256:4c908a7036f0f3677f8afa577bcd973e3e20ddd2f7c42a33208d18bee95cdb6f \
--hash=sha256:51ab1ff33c19e336c02dee1e9fd1abd974a4ca3d8f7eef2a104d0816a241ce97 \
--hash=sha256:524133c1be38445c5c0575eacea42dbd44374b310b1ffc4b60ff01d881fabb96 \
--hash=sha256:57bb7e26928573ee7c6504f54c05860d867fd35e675769f3ce01b52af38d48e2 \
--hash=sha256:60f6665579e909b618282f3c14fa0b80570fbf1ee0e67678b9a9d43aa5d67a37 \
--hash=sha256:62224a9bb85b4b66d1b46d45cbe43d71cbf8f527d332b177e3b96191ffbc1e64 \
--hash=sha256:6770d7da00f358183d8fd5c4615436189e4f683bdb6affb02cad3d221d7bb757 \
--hash=sha256:6801aeddb6acb2c42eafa45bc1cb98ba236871ae6f33f31e984670b749a8e58e \
--hash=sha256:70d6b3ceaa9cc5a6ac52884f3b3d9544e8e231e95b23f138bdb78e6d4dc0eae3 \
--hash=sha256:78813b49d749e1bb4db1c57f2d4d7e6db22c253cb0a86ad819f5dc197710d4b2 \
--hash=sha256:841b2186adce48903c0fef235421ae21549020eca942c1da773ac380b056ab3c \
--hash=sha256:84fc186980231a287b28560d3123bd255d3c6b6659828c642b4cf961e2b923d0 \
--hash=sha256:885bde7d26e5b40e15c47bd5def48b38cbd50830a65f98122a8fb90962af7cd1 \
--hash=sha256:8b4309a2775e4feee7356e63b163969a215d663399cce1b3d3b65e7ec2d9680e \
--hash=sha256:8d77f92438daeaddc05682f0f3dac90c5b9829bcac75b57e8ce09cb67786073c \
--hash=sha256:902425f5afe28572d65d2bf9c33edd5265c612ff82c69e6f83ea13eafc0dcbea \
--hash=sha256:9bcc1e77fbd1609198966ded6b2a9897bd6c6bcbd2287a2fc7d75f1a254179c5 \
--hash=sha256:a408c3c51358c89b29cfa5317cf11518b7ce5de1717abb55c5ae2d2921027de6 \
--hash=sha256:a9bf8adc9e1f3012edc8f09b08336272aec0c55bc677422273e21280db748f7c \
--hash=sha256:b818db35879d2edf7f46c7e729c700a0bce03b61b9412f5a7118406687cb151d \
--hash=sha256:b8974b2a266b54c96709bd5e239979cddfd2dbceed331aa567ea1d7c4a2202db \
--hash=sha256:be392ec3529e2f57faa28709d60723a763904f71a2b63aabe14fee6648fe3b14 \
--hash=sha256:d3972b13148c1d1fbc092b27678a33b3080d1ac0ca305742b0119b75f9e87e38 \
--hash=sha256:d40dcf533ca481355aa7b682e9e079f766f35715defa4929aeb5597f9604272e \
--hash=sha256:e93df708c69a193fc7987192f94df250f83f3851fda49413f02ba5dded639482 \
--hash=sha256:efd7e6660674e234e29937bc1481dceb7e0336bfae75b856b4fb272b5093c5d4 \
--hash=sha256:f9b3a78f69dcbd803cf2fb3f972779875b244c1115481dfbdd567b2c22b31f6b \
--hash=sha256:fa39475eaccb98f9199eccfda4298abaf35ae0caec676ffc25b3a5e224044464 \
--hash=sha256:fbce6dae41b692a5973d0f2158f782b9ad05babc2c2019a970a1094a23909b1b
fonttools==4.60.1 \
--hash=sha256:022beaea4b73a70295b688f817ddc24ed3e3418b5036ffcd5658141184ef0d0c \
--hash=sha256:026290e4ec76583881763fac284aca67365e0be9f13a7fb137257096114cb3bc \
--hash=sha256:0b0835ed15dd5b40d726bb61c846a688f5b4ce2208ec68779bc81860adb5851a \
--hash=sha256:0eae96373e4b7c9e45d099d7a523444e3554360927225c1cdae221a58a45b856 \
--hash=sha256:122e1a8ada290423c493491d002f622b1992b1ab0b488c68e31c413390dc7eb2 \
--hash=sha256:1410155d0e764a4615774e5c2c6fc516259fe3eca5882f034eb9bfdbee056259 \
--hash=sha256:145daa14bf24824b677b9357c5e44fd8895c2a8f53596e1b9ea3496081dc692c \
--hash=sha256:1525796c3ffe27bb6268ed2a1bb0dcf214d561dfaf04728abf01489eb5339dce \
--hash=sha256:154cb6ee417e417bf5f7c42fe25858c9140c26f647c7347c06f0cc2d47eff003 \
--hash=sha256:2299df884c11162617a66b7c316957d74a18e3758c0274762d2cc87df7bc0272 \
--hash=sha256:2409d5fb7b55fd70f715e6d34e7a6e4f7511b8ad29a49d6df225ee76da76dd77 \
--hash=sha256:268ecda8ca6cb5c4f044b1fb9b3b376e8cd1b361cef275082429dc4174907038 \
--hash=sha256:282dafa55f9659e8999110bd8ed422ebe1c8aecd0dc396550b038e6c9a08b8ea \
--hash=sha256:2ee06fc57512144d8b0445194c2da9f190f61ad51e230f14836286470c99f854 \
--hash=sha256:3630e86c484263eaac71d117085d509cbcf7b18f677906824e4bace598fb70d2 \
--hash=sha256:398447f3d8c0c786cbf1209711e79080a40761eb44b27cdafffb48f52bcec258 \
--hash=sha256:4ba4bd646e86de16160f0fb72e31c3b9b7d0721c3e5b26b9fa2fc931dfdb2652 \
--hash=sha256:5664fd1a9ea7f244487ac8f10340c4e37664675e8667d6fee420766e0fb3cf08 \
--hash=sha256:583b7f8e3c49486e4d489ad1deacfb8d5be54a8ef34d6df824f6a171f8511d99 \
--hash=sha256:596ecaca36367027d525b3b426d8a8208169d09edcf8c7506aceb3a38bfb55c7 \
--hash=sha256:5c1015318e4fec75dd4943ad5f6a206d9727adf97410d58b7e32ab644a807914 \
--hash=sha256:66929e2ea2810c6533a5184f938502cfdaea4bc3efb7130d8cc02e1c1b4108d6 \
--hash=sha256:6ec722ee589e89a89f5b7574f5c45604030aa6ae24cb2c751e2707193b466fed \
--hash=sha256:6f68576bb4bbf6060c7ab047b1574a1ebe5c50a17de62830079967b211059ebb \
--hash=sha256:7473a8ed9ed09aeaa191301244a5a9dbe46fe0bf54f9d6cd21d83044c3321217 \
--hash=sha256:7b0c6d57ab00dae9529f3faf187f2254ea0aa1e04215cf2f1a8ec277c96661bc \
--hash=sha256:7b4c32e232a71f63a5d00259ca3d88345ce2a43295bb049d21061f338124246f \
--hash=sha256:8177ec9676ea6e1793c8a084a90b65a9f778771998eb919d05db6d4b1c0b114c \
--hash=sha256:839565cbf14645952d933853e8ade66a463684ed6ed6c9345d0faf1f0e868877 \
--hash=sha256:875cb7764708b3132637f6c5fb385b16eeba0f7ac9fa45a69d35e09b47045801 \
--hash=sha256:8a44788d9d91df72d1a5eac49b31aeb887a5f4aab761b4cffc4196c74907ea85 \
--hash=sha256:8b4eb332f9501cb1cd3d4d099374a1e1306783ff95489a1026bde9eb02ccc34a \
--hash=sha256:906306ac7afe2156fcf0042173d6ebbb05416af70f6b370967b47f8f00103bbb \
--hash=sha256:992775c9fbe2cf794786fa0ffca7f09f564ba3499b8fe9f2f80bd7197db60383 \
--hash=sha256:996a4d1834524adbb423385d5a629b868ef9d774670856c63c9a0408a3063401 \
--hash=sha256:9a52f254ce051e196b8fe2af4634c2d2f02c981756c6464dc192f1b6050b4e28 \
--hash=sha256:9d0ced62b59e0430b3690dbc5373df1c2aa7585e9a8ce38eff87f0fd993c5b01 \
--hash=sha256:a140761c4ff63d0cb9256ac752f230460ee225ccef4ad8f68affc723c88e2036 \
--hash=sha256:a184b2ea57b13680ab6d5fbde99ccef152c95c06746cb7718c583abd8f945ccc \
--hash=sha256:a3db56f153bd4c5c2b619ab02c5db5192e222150ce5a1bc10f16164714bc39ac \
--hash=sha256:a46b2f450bc79e06ef3b6394f0c68660529ed51692606ad7f953fc2e448bc903 \
--hash=sha256:a884aef09d45ba1206712c7dbda5829562d3fea7726935d3289d343232ecb0d3 \
--hash=sha256:b2cf105cee600d2de04ca3cfa1f74f1127f8455b71dbad02b9da6ec266e116d6 \
--hash=sha256:b33a7884fabd72bdf5f910d0cf46be50dce86a0362a65cfc746a4168c67eb96c \
--hash=sha256:b42d86938e8dda1cd9a1a87a6d82f1818eaf933348429653559a458d027446da \
--hash=sha256:b6379e7546ba4ae4b18f8ae2b9bc5960936007a1c0e30b342f662577e8bc3299 \
--hash=sha256:c7420a2696a44650120cdd269a5d2e56a477e2bfa9d95e86229059beb1c19e15 \
--hash=sha256:c8651e0d4b3bdeda6602b85fdc2abbefc1b41e573ecb37b6779c4ca50753a199 \
--hash=sha256:d066ea419f719ed87bc2c99a4a4bfd77c2e5949cb724588b9dd58f3fd90b92bf \
--hash=sha256:e6c58beb17380f7c2ea181ea11e7db8c0ceb474c9dd45f48e71e2cb577d146a1 \
--hash=sha256:e852d9dda9f93ad3651ae1e3bb770eac544ec93c3807888798eccddf84596537 \
--hash=sha256:ec3681a0cb34c255d76dd9d865a55f260164adb9fa02628415cdc2d43ee2c05d \
--hash=sha256:ee0c0b3b35b34f782afc673d503167157094a16f442ace7c6c5e0ca80b08f50c \
--hash=sha256:eedacb5c5d22b7097482fa834bda0dafa3d914a4e829ec83cdea2a01f8c813c4 \
--hash=sha256:ef00af0439ebfee806b25f24c8f92109157ff3fac5731dc7867957812e87b8d9 \
--hash=sha256:f0e8817c7d1a0c2eedebf57ef9a9896f3ea23324769a9a2061a80fe8852705ed \
--hash=sha256:f3d5be054c461d6a2268831f04091dc82753176f6ea06dc6047a5e168265a987 \
--hash=sha256:f4b5c37a5f40e4d733d3bbaaef082149bee5a5ea3156a785ff64d949bd1353fa
# via weasyprint
identify==2.6.13 \
--hash=sha256:60381139b3ae39447482ecc406944190f690d4a2997f2584062089848361b33b \
--hash=sha256:da8d6c828e773620e13bfa86ea601c5a5310ba4bcd65edf378198b56a1f9fb32
identify==2.6.15 \
--hash=sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757 \
--hash=sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf
# via pre-commit
nodeenv==1.9.1 \
--hash=sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f \
@@ -338,128 +251,113 @@ pdf2image==1.17.0 \
--hash=sha256:eaa959bc116b420dd7ec415fcae49b98100dda3dd18cd2fdfa86d09f112f6d57 \
--hash=sha256:ecdd58d7afb810dffe21ef2b1bbc057ef434dabbac6c33778a38a3f7744a27e2
# via -r .github/scripts/requirements_dev.in
pillow==11.3.0 \
--hash=sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2 \
--hash=sha256:02a723e6bf909e7cea0dac1b0e0310be9d7650cd66222a5f1c571455c0a45214 \
--hash=sha256:040a5b691b0713e1f6cbe222e0f4f74cd233421e105850ae3b3c0ceda520f42e \
--hash=sha256:05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59 \
--hash=sha256:068d9c39a2d1b358eb9f245ce7ab1b5c3246c7c8c7d9ba58cfa5b43146c06e50 \
--hash=sha256:0743841cabd3dba6a83f38a92672cccbd69af56e3e91777b0ee7f4dba4385632 \
--hash=sha256:092c80c76635f5ecb10f3f83d76716165c96f5229addbd1ec2bdbbda7d496e06 \
--hash=sha256:0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a \
--hash=sha256:0bce5c4fd0921f99d2e858dc4d4d64193407e1b99478bc5cacecba2311abde51 \
--hash=sha256:1019b04af07fc0163e2810167918cb5add8d74674b6267616021ab558dc98ced \
--hash=sha256:106064daa23a745510dabce1d84f29137a37224831d88eb4ce94bb187b1d7e5f \
--hash=sha256:118ca10c0d60b06d006be10a501fd6bbdfef559251ed31b794668ed569c87e12 \
--hash=sha256:13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8 \
--hash=sha256:155658efb5e044669c08896c0c44231c5e9abcaadbc5cd3648df2f7c0b96b9a6 \
--hash=sha256:1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580 \
--hash=sha256:19d2ff547c75b8e3ff46f4d9ef969a06c30ab2d4263a9e287733aa8b2429ce8f \
--hash=sha256:1a992e86b0dd7aeb1f053cd506508c0999d710a8f07b4c791c63843fc6a807ac \
--hash=sha256:1b9c17fd4ace828b3003dfd1e30bff24863e0eb59b535e8f80194d9cc7ecf860 \
--hash=sha256:1c627742b539bba4309df89171356fcb3cc5a9178355b2727d1b74a6cf155fbd \
--hash=sha256:1cd110edf822773368b396281a2293aeb91c90a2db00d78ea43e7e861631b722 \
--hash=sha256:1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8 \
--hash=sha256:23cff760a9049c502721bdb743a7cb3e03365fafcdfc2ef9784610714166e5a4 \
--hash=sha256:2465a69cf967b8b49ee1b96d76718cd98c4e925414ead59fdf75cf0fd07df673 \
--hash=sha256:2a3117c06b8fb646639dce83694f2f9eac405472713fcb1ae887469c0d4f6788 \
--hash=sha256:2aceea54f957dd4448264f9bf40875da0415c83eb85f55069d89c0ed436e3542 \
--hash=sha256:2d6fcc902a24ac74495df63faad1884282239265c6839a0a6416d33faedfae7e \
--hash=sha256:30807c931ff7c095620fe04448e2c2fc673fcbb1ffe2a7da3fb39613489b1ddd \
--hash=sha256:30b7c02f3899d10f13d7a48163c8969e4e653f8b43416d23d13d1bbfdc93b9f8 \
--hash=sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523 \
--hash=sha256:3cee80663f29e3843b68199b9d6f4f54bd1d4a6b59bdd91bceefc51238bcb967 \
--hash=sha256:3e184b2f26ff146363dd07bde8b711833d7b0202e27d13540bfe2e35a323a809 \
--hash=sha256:41342b64afeba938edb034d122b2dda5db2139b9a4af999729ba8818e0056477 \
--hash=sha256:41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027 \
--hash=sha256:4445fa62e15936a028672fd48c4c11a66d641d2c05726c7ec1f8ba6a572036ae \
--hash=sha256:45dfc51ac5975b938e9809451c51734124e73b04d0f0ac621649821a63852e7b \
--hash=sha256:465b9e8844e3c3519a983d58b80be3f668e2a7a5db97f2784e7079fbc9f9822c \
--hash=sha256:48d254f8a4c776de343051023eb61ffe818299eeac478da55227d96e241de53f \
--hash=sha256:4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e \
--hash=sha256:4c96f993ab8c98460cd0c001447bff6194403e8b1d7e149ade5f00594918128b \
--hash=sha256:504b6f59505f08ae014f724b6207ff6222662aab5cc9542577fb084ed0676ac7 \
--hash=sha256:527b37216b6ac3a12d7838dc3bd75208ec57c1c6d11ef01902266a5a0c14fc27 \
--hash=sha256:5418b53c0d59b3824d05e029669efa023bbef0f3e92e75ec8428f3799487f361 \
--hash=sha256:59a03cdf019efbfeeed910bf79c7c93255c3d54bc45898ac2a4140071b02b4ae \
--hash=sha256:5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d \
--hash=sha256:6359a3bc43f57d5b375d1ad54a0074318a0844d11b76abccf478c37c986d3cfc \
--hash=sha256:643f189248837533073c405ec2f0bb250ba54598cf80e8c1e043381a60632f58 \
--hash=sha256:65dc69160114cdd0ca0f35cb434633c75e8e7fad4cf855177a05bf38678f73ad \
--hash=sha256:67172f2944ebba3d4a7b54f2e95c786a3a50c21b88456329314caaa28cda70f6 \
--hash=sha256:676b2815362456b5b3216b4fd5bd89d362100dc6f4945154ff172e206a22c024 \
--hash=sha256:6a418691000f2a418c9135a7cf0d797c1bb7d9a485e61fe8e7722845b95ef978 \
--hash=sha256:6abdbfd3aea42be05702a8dd98832329c167ee84400a1d1f61ab11437f1717eb \
--hash=sha256:6be31e3fc9a621e071bc17bb7de63b85cbe0bfae91bb0363c893cbe67247780d \
--hash=sha256:7107195ddc914f656c7fc8e4a5e1c25f32e9236ea3ea860f257b0436011fddd0 \
--hash=sha256:71f511f6b3b91dd543282477be45a033e4845a40278fa8dcdbfdb07109bf18f9 \
--hash=sha256:7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f \
--hash=sha256:7966e38dcd0fa11ca390aed7c6f20454443581d758242023cf36fcb319b1a874 \
--hash=sha256:79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa \
--hash=sha256:7aee118e30a4cf54fdd873bd3a29de51e29105ab11f9aad8c32123f58c8f8081 \
--hash=sha256:7b161756381f0918e05e7cb8a371fff367e807770f8fe92ecb20d905d0e1c149 \
--hash=sha256:7c8ec7a017ad1bd562f93dbd8505763e688d388cde6e4a010ae1486916e713e6 \
--hash=sha256:7d1aa4de119a0ecac0a34a9c8bde33f34022e2e8f99104e47a3ca392fd60e37d \
--hash=sha256:7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd \
--hash=sha256:819931d25e57b513242859ce1876c58c59dc31587847bf74cfe06b2e0cb22d2f \
--hash=sha256:83e1b0161c9d148125083a35c1c5a89db5b7054834fd4387499e06552035236c \
--hash=sha256:857844335c95bea93fb39e0fa2726b4d9d758850b34075a7e3ff4f4fa3aa3b31 \
--hash=sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e \
--hash=sha256:8924748b688aa210d79883357d102cd64690e56b923a186f35a82cbc10f997db \
--hash=sha256:89bd777bc6624fe4115e9fac3352c79ed60f3bb18651420635f26e643e3dd1f6 \
--hash=sha256:8dc70ca24c110503e16918a658b869019126ecfe03109b754c402daff12b3d9f \
--hash=sha256:91da1d88226663594e3f6b4b8c3c8d85bd504117d043740a8e0ec449087cc494 \
--hash=sha256:921bd305b10e82b4d1f5e802b6850677f965d8394203d182f078873851dada69 \
--hash=sha256:932c754c2d51ad2b2271fd01c3d121daaa35e27efae2a616f77bf164bc0b3e94 \
--hash=sha256:93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77 \
--hash=sha256:97afb3a00b65cc0804d1c7abddbf090a81eaac02768af58cbdcaaa0a931e0b6d \
--hash=sha256:97f07ed9f56a3b9b5f49d3661dc9607484e85c67e27f3e8be2c7d28ca032fec7 \
--hash=sha256:98a9afa7b9007c67ed84c57c9e0ad86a6000da96eaa638e4f8abe5b65ff83f0a \
--hash=sha256:9ab6ae226de48019caa8074894544af5b53a117ccb9d3b3dcb2871464c829438 \
--hash=sha256:9c412fddd1b77a75aa904615ebaa6001f169b26fd467b4be93aded278266b288 \
--hash=sha256:a1bc6ba083b145187f648b667e05a2534ecc4b9f2784c2cbe3089e44868f2b9b \
--hash=sha256:a418486160228f64dd9e9efcd132679b7a02a5f22c982c78b6fc7dab3fefb635 \
--hash=sha256:a4d336baed65d50d37b88ca5b60c0fa9d81e3a87d4a7930d3880d1624d5b31f3 \
--hash=sha256:a6444696fce635783440b7f7a9fc24b3ad10a9ea3f0ab66c5905be1c19ccf17d \
--hash=sha256:a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe \
--hash=sha256:b4b8f3efc8d530a1544e5962bd6b403d5f7fe8b9e08227c6b255f98ad82b4ba0 \
--hash=sha256:b5f56c3f344f2ccaf0dd875d3e180f631dc60a51b314295a3e681fe8cf851fbe \
--hash=sha256:be5463ac478b623b9dd3937afd7fb7ab3d79dd290a28e2b6df292dc75063eb8a \
--hash=sha256:c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805 \
--hash=sha256:c84d689db21a1c397d001aa08241044aa2069e7587b398c8cc63020390b1c1b8 \
--hash=sha256:c96d333dcf42d01f47b37e0979b6bd73ec91eae18614864622d9b87bbd5bbf36 \
--hash=sha256:cadc9e0ea0a2431124cde7e1697106471fc4c1da01530e679b2391c37d3fbb3a \
--hash=sha256:cc3e831b563b3114baac7ec2ee86819eb03caa1a2cef0b481a5675b59c4fe23b \
--hash=sha256:cd8ff254faf15591e724dc7c4ddb6bf4793efcbe13802a4ae3e863cd300b493e \
--hash=sha256:d000f46e2917c705e9fb93a3606ee4a819d1e3aa7a9b442f6444f07e77cf5e25 \
--hash=sha256:d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12 \
--hash=sha256:e5c5858ad8ec655450a7c7df532e9842cf8df7cc349df7225c60d5d348c8aada \
--hash=sha256:e67d793d180c9df62f1f40aee3accca4829d3794c95098887edc18af4b8b780c \
--hash=sha256:ea944117a7974ae78059fcc1800e5d3295172bb97035c0c1d9345fca1419da71 \
--hash=sha256:eb76541cba2f958032d79d143b98a3a6b3ea87f0959bbe256c0b5e416599fd5d \
--hash=sha256:ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c \
--hash=sha256:ee92f2fd10f4adc4b43d07ec5e779932b4eb3dbfbc34790ada5a6669bc095aa6 \
--hash=sha256:f0f5d8f4a08090c6d6d578351a2b91acf519a54986c055af27e7a93feae6d3f1 \
--hash=sha256:f1f182ebd2303acf8c380a54f615ec883322593320a9b00438eb842c1f37ae50 \
--hash=sha256:f8a5827f84d973d8636e9dc5764af4f0cf2318d26744b3d902931701b0d46653 \
--hash=sha256:f944255db153ebb2b19c51fe85dd99ef0ce494123f21b9db4877ffdfc5590c7c \
--hash=sha256:fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4 \
--hash=sha256:fe27fb049cdcca11f11a7bfda64043c37b30e6b91f10cb5bab275806c32f6ab3
pillow==12.0.0 \
--hash=sha256:0869154a2d0546545cde61d1789a6524319fc1897d9ee31218eae7a60ccc5643 \
--hash=sha256:09f2d0abef9e4e2f349305a4f8cc784a8a6c2f58a8c4892eea13b10a943bd26e \
--hash=sha256:0b817e7035ea7f6b942c13aa03bb554fc44fea70838ea21f8eb31c638326584e \
--hash=sha256:0fd00cac9c03256c8b2ff58f162ebcd2587ad3e1f2e397eab718c47e24d231cc \
--hash=sha256:110486b79f2d112cf6add83b28b627e369219388f64ef2f960fef9ebaf54c642 \
--hash=sha256:1979f4566bb96c1e50a62d9831e2ea2d1211761e5662afc545fa766f996632f6 \
--hash=sha256:1ac11e8ea4f611c3c0147424eae514028b5e9077dd99ab91e1bd7bc33ff145e1 \
--hash=sha256:1b1b133e6e16105f524a8dec491e0586d072948ce15c9b914e41cdadd209052b \
--hash=sha256:1ee80a59f6ce048ae13cda1abf7fbd2a34ab9ee7d401c46be3ca685d1999a399 \
--hash=sha256:21f241bdd5080a15bc86d3466a9f6074a9c2c2b314100dd896ac81ee6db2f1ba \
--hash=sha256:266cd5f2b63ff316d5a1bba46268e603c9caf5606d44f38c2873c380950576ad \
--hash=sha256:26d9f7d2b604cd23aba3e9faf795787456ac25634d82cd060556998e39c6fa47 \
--hash=sha256:27f95b12453d165099c84f8a8bfdfd46b9e4bda9e0e4b65f0635430027f55739 \
--hash=sha256:2c54c1a783d6d60595d3514f0efe9b37c8808746a66920315bfd34a938d7994b \
--hash=sha256:2fa5f0b6716fc88f11380b88b31fe591a06c6315e955c096c35715788b339e3f \
--hash=sha256:32ed80ea8a90ee3e6fa08c21e2e091bba6eda8eccc83dbc34c95169507a91f10 \
--hash=sha256:3830c769decf88f1289680a59d4f4c46c72573446352e2befec9a8512104fa52 \
--hash=sha256:38df9b4bfd3db902c9c2bd369bcacaf9d935b2fff73709429d95cc41554f7b3d \
--hash=sha256:3adfb466bbc544b926d50fe8f4a4e6abd8c6bffd28a26177594e6e9b2b76572b \
--hash=sha256:3e42edad50b6909089750e65c91aa09aaf1e0a71310d383f11321b27c224ed8a \
--hash=sha256:4078242472387600b2ce8d93ade8899c12bf33fa89e55ec89fe126e9d6d5d9e9 \
--hash=sha256:455247ac8a4cfb7b9bc45b7e432d10421aea9fc2e74d285ba4072688a74c2e9d \
--hash=sha256:4cc6b3b2efff105c6a1656cfe59da4fdde2cda9af1c5e0b58529b24525d0a098 \
--hash=sha256:4cf7fed4b4580601c4345ceb5d4cbf5a980d030fd5ad07c4d2ec589f95f09905 \
--hash=sha256:5193fde9a5f23c331ea26d0cf171fbf67e3f247585f50c08b3e205c7aeb4589b \
--hash=sha256:5269cc1caeedb67e6f7269a42014f381f45e2e7cd42d834ede3c703a1d915fe3 \
--hash=sha256:53561a4ddc36facb432fae7a9d8afbfaf94795414f5cdc5fc52f28c1dca90371 \
--hash=sha256:55f818bd74fe2f11d4d7cbc65880a843c4075e0ac7226bc1a23261dbea531953 \
--hash=sha256:58eea5ebe51504057dd95c5b77d21700b77615ab0243d8152793dc00eb4faf01 \
--hash=sha256:5d5c411a8eaa2299322b647cd932586b1427367fd3184ffbb8f7a219ea2041ca \
--hash=sha256:6846bd2d116ff42cba6b646edf5bf61d37e5cbd256425fa089fee4ff5c07a99e \
--hash=sha256:6ace95230bfb7cd79ef66caa064bbe2f2a1e63d93471c3a2e1f1348d9f22d6b7 \
--hash=sha256:6e51b71417049ad6ab14c49608b4a24d8fb3fe605e5dfabfe523b58064dc3d27 \
--hash=sha256:71db6b4c1653045dacc1585c1b0d184004f0d7e694c7b34ac165ca70c0838082 \
--hash=sha256:7438839e9e053ef79f7112c881cef684013855016f928b168b81ed5835f3e75e \
--hash=sha256:759de84a33be3b178a64c8ba28ad5c135900359e85fb662bc6e403ad4407791d \
--hash=sha256:792a2c0be4dcc18af9d4a2dfd8a11a17d5e25274a1062b0ec1c2d79c76f3e7f8 \
--hash=sha256:7d87ef5795da03d742bf49439f9ca4d027cde49c82c5371ba52464aee266699a \
--hash=sha256:7dfb439562f234f7d57b1ac6bc8fe7f838a4bd49c79230e0f6a1da93e82f1fad \
--hash=sha256:7fa22993bac7b77b78cae22bad1e2a987ddf0d9015c63358032f84a53f23cdc3 \
--hash=sha256:805ebf596939e48dbb2e4922a1d3852cfc25c38160751ce02da93058b48d252a \
--hash=sha256:82240051c6ca513c616f7f9da06e871f61bfd7805f566275841af15015b8f98d \
--hash=sha256:87d4f8125c9988bfbed67af47dd7a953e2fc7b0cc1e7800ec6d2080d490bb353 \
--hash=sha256:8d8ca2b210ada074d57fcee40c30446c9562e542fc46aedc19baf758a93532ee \
--hash=sha256:8dc232e39d409036af549c86f24aed8273a40ffa459981146829a324e0848b4b \
--hash=sha256:90387104ee8400a7b4598253b4c406f8958f59fcf983a6cea2b50d59f7d63d0b \
--hash=sha256:905b0365b210c73afb0ebe9101a32572152dfd1c144c7e28968a331b9217b94a \
--hash=sha256:99353a06902c2e43b43e8ff74ee65a7d90307d82370604746738a1e0661ccca7 \
--hash=sha256:99a7f72fb6249302aa62245680754862a44179b545ded638cf1fef59befb57ef \
--hash=sha256:9f0b04c6b8584c2c193babcccc908b38ed29524b29dd464bc8801bf10d746a3a \
--hash=sha256:9fe611163f6303d1619bbcb653540a4d60f9e55e622d60a3108be0d5b441017a \
--hash=sha256:a3475b96f5908b3b16c47533daaa87380c491357d197564e0ba34ae75c0f3257 \
--hash=sha256:a6597ff2b61d121172f5844b53f21467f7082f5fb385a9a29c01414463f93b07 \
--hash=sha256:a7921c5a6d31b3d756ec980f2f47c0cfdbce0fc48c22a39347a895f41f4a6ea4 \
--hash=sha256:aa5129de4e174daccbc59d0a3b6d20eaf24417d59851c07ebb37aeb02947987c \
--hash=sha256:aeaefa96c768fc66818730b952a862235d68825c178f1b3ffd4efd7ad2edcb7c \
--hash=sha256:afbefa430092f71a9593a99ab6a4e7538bc9eabbf7bf94f91510d3503943edc4 \
--hash=sha256:aff9e4d82d082ff9513bdd6acd4f5bd359f5b2c870907d2b0a9c5e10d40c88fe \
--hash=sha256:b22bd8c974942477156be55a768f7aa37c46904c175be4e158b6a86e3a6b7ca8 \
--hash=sha256:b290fd8aa38422444d4b50d579de197557f182ef1068b75f5aa8558638b8d0a5 \
--hash=sha256:b2e4b27a6e15b04832fe9bf292b94b5ca156016bbc1ea9c2c20098a0320d6cf6 \
--hash=sha256:b583dc9070312190192631373c6c8ed277254aa6e6084b74bdd0a6d3b221608e \
--hash=sha256:b87843e225e74576437fd5b6a4c2205d422754f84a06942cfaf1dc32243e45a8 \
--hash=sha256:bc91a56697869546d1b8f0a3ff35224557ae7f881050e99f615e0119bf934b4e \
--hash=sha256:bd87e140e45399c818fac4247880b9ce719e4783d767e030a883a970be632275 \
--hash=sha256:bde737cff1a975b70652b62d626f7785e0480918dece11e8fef3c0cf057351c3 \
--hash=sha256:bdee52571a343d721fb2eb3b090a82d959ff37fc631e3f70422e0c2e029f3e76 \
--hash=sha256:bee2a6db3a7242ea309aa7ee8e2780726fed67ff4e5b40169f2c940e7eb09227 \
--hash=sha256:beeae3f27f62308f1ddbcfb0690bf44b10732f2ef43758f169d5e9303165d3f9 \
--hash=sha256:c50f36a62a22d350c96e49ad02d0da41dbd17ddc2e29750dbdba4323f85eb4a5 \
--hash=sha256:c607c90ba67533e1b2355b821fef6764d1dd2cbe26b8c1005ae84f7aea25ff79 \
--hash=sha256:c7b2a63fd6d5246349f3d3f37b14430d73ee7e8173154461785e43036ffa96ca \
--hash=sha256:c828a1ae702fc712978bda0320ba1b9893d99be0badf2647f693cc01cf0f04fa \
--hash=sha256:c85de1136429c524e55cfa4e033b4a7940ac5c8ee4d9401cc2d1bf48154bbc7b \
--hash=sha256:c98fa880d695de164b4135a52fd2e9cd7b7c90a9d8ac5e9e443a24a95ef9248e \
--hash=sha256:cae81479f77420d217def5f54b5b9d279804d17e982e0f2fa19b1d1e14ab5197 \
--hash=sha256:d034140032870024e6b9892c692fe2968493790dd57208b2c37e3fb35f6df3ab \
--hash=sha256:d120c38a42c234dc9a8c5de7ceaaf899cf33561956acb4941653f8bdc657aa79 \
--hash=sha256:d4827615da15cd59784ce39d3388275ec093ae3ee8d7f0c089b76fa87af756c2 \
--hash=sha256:d49e2314c373f4c2b39446fb1a45ed333c850e09d0c59ac79b72eb3b95397363 \
--hash=sha256:d52610d51e265a51518692045e372a4c363056130d922a7351429ac9f27e70b0 \
--hash=sha256:d64317d2587c70324b79861babb9c09f71fbb780bad212018874b2c013d8600e \
--hash=sha256:d77153e14b709fd8b8af6f66a3afbb9ed6e9fc5ccf0b6b7e1ced7b036a228782 \
--hash=sha256:d7e091d464ac59d2c7ad8e7e08105eaf9dafbc3883fd7265ffccc2baad6ac925 \
--hash=sha256:dd333073e0cacdc3089525c7df7d39b211bcdf31fc2824e49d01c6b6187b07d0 \
--hash=sha256:e5d8efac84c9afcb40914ab49ba063d94f5dbdf5066db4482c66a992f47a3a3b \
--hash=sha256:f135c702ac42262573fe9714dfe99c944b4ba307af5eb507abef1667e2cbbced \
--hash=sha256:f13711b1a5ba512d647a0e4ba79280d3a9a045aaf7e0cc6fbe96b91d4cdf6b0c \
--hash=sha256:f4f1231b7dec408e8670264ce63e9c71409d9583dd21d32c163e25213ee2a344 \
--hash=sha256:fa3ed2a29a9e9d2d488b4da81dcb54720ac3104a20bf0bd273f1e4648aff5af9 \
--hash=sha256:fb3096c30df99fd01c7bf8e544f392103d0795b9f98ba71a8054bcbf56b255f1
# via
# -r .github/scripts/requirements_dev.in
# pdf2image
# weasyprint
platformdirs==4.3.8 \
--hash=sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc \
--hash=sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4
platformdirs==4.5.0 \
--hash=sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312 \
--hash=sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3
# via virtualenv
pre-commit==4.3.0 \
--hash=sha256:2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8 \
--hash=sha256:499fe450cc9d42e9d58e606262795ecb64dd05438943c62b66f6a8673da30b16
# via -r .github/scripts/requirements_dev.in
pycparser==2.22 \
--hash=sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6 \
--hash=sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc
pycparser==2.23 \
--hash=sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2 \
--hash=sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934
# via cffi
pydyf==0.11.0 \
--hash=sha256:0aaf9e2ebbe786ec7a78ec3fbffa4cdcecde53fd6f563221d53c6bc1328848a3 \
@@ -469,60 +367,80 @@ pyphen==0.17.2 \
--hash=sha256:3a07fb017cb2341e1d9ff31b8634efb1ae4dc4b130468c7c39dd3d32e7c3affd \
--hash=sha256:f60647a9c9b30ec6c59910097af82bc5dd2d36576b918e44148d8b07ef3b4aa3
# via weasyprint
pyyaml==6.0.2 \
--hash=sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff \
--hash=sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48 \
--hash=sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086 \
--hash=sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e \
--hash=sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133 \
--hash=sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5 \
--hash=sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484 \
--hash=sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee \
--hash=sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5 \
--hash=sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68 \
--hash=sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a \
--hash=sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf \
--hash=sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99 \
--hash=sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8 \
--hash=sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85 \
--hash=sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19 \
--hash=sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc \
--hash=sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a \
--hash=sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1 \
--hash=sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317 \
--hash=sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c \
--hash=sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631 \
--hash=sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d \
--hash=sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652 \
--hash=sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5 \
--hash=sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e \
--hash=sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b \
--hash=sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8 \
--hash=sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476 \
--hash=sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706 \
--hash=sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563 \
--hash=sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237 \
--hash=sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b \
--hash=sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083 \
--hash=sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180 \
--hash=sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425 \
--hash=sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e \
--hash=sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f \
--hash=sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725 \
--hash=sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183 \
--hash=sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab \
--hash=sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774 \
--hash=sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725 \
--hash=sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e \
--hash=sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5 \
--hash=sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d \
--hash=sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290 \
--hash=sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44 \
--hash=sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed \
--hash=sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4 \
--hash=sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba \
--hash=sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12 \
--hash=sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4
pyyaml==6.0.3 \
--hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \
--hash=sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a \
--hash=sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3 \
--hash=sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956 \
--hash=sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6 \
--hash=sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c \
--hash=sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65 \
--hash=sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a \
--hash=sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0 \
--hash=sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b \
--hash=sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1 \
--hash=sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6 \
--hash=sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7 \
--hash=sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e \
--hash=sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007 \
--hash=sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310 \
--hash=sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4 \
--hash=sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9 \
--hash=sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295 \
--hash=sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea \
--hash=sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0 \
--hash=sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e \
--hash=sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac \
--hash=sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9 \
--hash=sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7 \
--hash=sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35 \
--hash=sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb \
--hash=sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b \
--hash=sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69 \
--hash=sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5 \
--hash=sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b \
--hash=sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c \
--hash=sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369 \
--hash=sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd \
--hash=sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824 \
--hash=sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198 \
--hash=sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065 \
--hash=sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c \
--hash=sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c \
--hash=sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764 \
--hash=sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196 \
--hash=sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b \
--hash=sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00 \
--hash=sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac \
--hash=sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8 \
--hash=sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e \
--hash=sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28 \
--hash=sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3 \
--hash=sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5 \
--hash=sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4 \
--hash=sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b \
--hash=sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf \
--hash=sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5 \
--hash=sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702 \
--hash=sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8 \
--hash=sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788 \
--hash=sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da \
--hash=sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d \
--hash=sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc \
--hash=sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c \
--hash=sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba \
--hash=sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f \
--hash=sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917 \
--hash=sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5 \
--hash=sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26 \
--hash=sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f \
--hash=sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b \
--hash=sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be \
--hash=sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c \
--hash=sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3 \
--hash=sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6 \
--hash=sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926 \
--hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0
# via pre-commit
tinycss2==1.4.0 \
--hash=sha256:10c0972f6fc0fbee87c3edb76549357415e94548c1ae10ebccdea16fb404a9b7 \
@@ -534,13 +452,13 @@ tinyhtml5==2.0.0 \
--hash=sha256:086f998833da24c300c414d9fe81d9b368fd04cb9d2596a008421cbc705fcfcc \
--hash=sha256:13683277c5b176d070f82d099d977194b7a1e26815b016114f581a74bbfbf47e
# via weasyprint
unoserver==3.3.2 \
--hash=sha256:1eeb7467cf6b56b8eff3b576e2d1b2b2ff4e0eb2052e995ac80a1456de300639 \
--hash=sha256:87e144f903ee21951b2e06a97549450c13ed7eca5bcebad942d3352d4e882616
unoserver==3.4 \
--hash=sha256:3dcf2204013def1d1ddd3671f38b11346bdf349fef9728277462666a8a634419 \
--hash=sha256:64c24d33d4f65d680a2d9f676518cb28e7fd6c1f9d9a745c33e4a4cb59afdfcd
# via -r .github/scripts/requirements_dev.in
virtualenv==20.33.1 \
--hash=sha256:07c19bc66c11acab6a5958b815cbcee30891cd1c2ccf53785a28651a0d8d8a67 \
--hash=sha256:1b44478d9e261b3fb8baa5e74a0ca3bc0e05f21aa36167bf9cbf850e542765b8
virtualenv==20.35.4 \
--hash=sha256:643d3914d73d3eeb0c552cbb12d7e82adf0e504dbf86a3182f8771a153a1971c \
--hash=sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b
# via pre-commit
weasyprint==66.0 \
--hash=sha256:82b0783b726fcd318e2c977dcdddca76515b30044bc7a830cc4fbe717582a6d0 \
@@ -628,9 +546,9 @@ zopfli==0.2.3.post1 \
# via fonttools
# The following packages are considered to be unsafe in a requirements file:
pip==25.2 \
--hash=sha256:578283f006390f85bb6282dffb876454593d637f5d1be494b5202ce4877e71f2 \
--hash=sha256:6d67a2b4e7f14d8b31b8b52648866fa717f45a1eb70e83002f4331d07e953717
pip==25.3 \
--hash=sha256:8d0538dbbd7babbd207f261ed969c65de439f6bc9e5dbd3b3b9a77f25d95f343 \
--hash=sha256:9655943313a94722b7774661c21049070f6bbb0a1516bf02f7c8d5d9201514cd
# via -r .github/scripts/requirements_dev.in
setuptools==80.9.0 \
--hash=sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922 \
+9 -9
View File
@@ -12,9 +12,9 @@ distlib==0.4.0 \
--hash=sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16 \
--hash=sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d
# via virtualenv
filelock==3.19.1 \
--hash=sha256:66eda1888b0171c998b35be2bcc0f6d75c388a7ce20c3f3f37aa8e96c2dddf58 \
--hash=sha256:d38e30481def20772f5baf097c122c3babc4fcdb7e14e57049eb9d88c6dc017d
filelock==3.20.0 \
--hash=sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2 \
--hash=sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4
# via virtualenv
identify==2.6.15 \
--hash=sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757 \
@@ -24,9 +24,9 @@ nodeenv==1.9.1 \
--hash=sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f \
--hash=sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9
# via pre-commit
platformdirs==4.4.0 \
--hash=sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85 \
--hash=sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf
platformdirs==4.5.0 \
--hash=sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312 \
--hash=sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3
# via virtualenv
pre-commit==4.3.0 \
--hash=sha256:2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8 \
@@ -107,7 +107,7 @@ pyyaml==6.0.3 \
--hash=sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926 \
--hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0
# via pre-commit
virtualenv==20.34.0 \
--hash=sha256:341f5afa7eee943e4984a9207c025feedd768baff6753cd660c857ceb3e36026 \
--hash=sha256:44815b2c9dee7ed86e387b842a84f20b93f7f417f95886ca1996a72a4138eb1a
virtualenv==20.35.4 \
--hash=sha256:643d3914d73d3eeb0c552cbb12d7e82adf0e504dbf86a3182f8771a153a1971c \
--hash=sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b
# via pre-commit
+11 -20
View File
@@ -37,7 +37,7 @@ jobs:
- name: Resolve PR info
id: resolve
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: |
const { owner, repo } = context.repo;
@@ -52,7 +52,6 @@ jobs:
core.setOutput('repository', pr.head.repo.full_name);
core.setOutput('ref', pr.head.ref);
core.setOutput('is_fork', String(pr.head.repo.fork));
core.setOutput('base_ref', pr.base.ref);
core.setOutput('author', pr.user.login);
core.setOutput('state', pr.state);
@@ -65,10 +64,6 @@ jobs:
IS_FORK: ${{ steps.resolve.outputs.is_fork }}
# nur bei workflow_dispatch gesetzt:
ALLOW_FORK_INPUT: ${{ inputs.allow_fork }}
# für Auto-PR-Logik:
PR_TITLE: ${{ github.event.pull_request.title }}
PR_BRANCH: ${{ github.event.pull_request.head.ref }}
PR_BASE: ${{ steps.resolve.outputs.base_ref }}
PR_AUTHOR: ${{ steps.resolve.outputs.author }}
run: |
set -e
@@ -89,14 +84,8 @@ jobs:
else
auth_users=("Frooodle" "sf298" "Ludy87" "LaserKaspar" "sbplat" "reecebrowne" "DarioGii" "ConnorYoh" "EthanHealy01" "jbrunton96" "balazs-szucs")
is_auth=false; for u in "${auth_users[@]}"; do [ "$u" = "$PR_AUTHOR" ] && is_auth=true && break; done
if [ "$PR_BASE" = "V2" ] && [ "$is_auth" = true ]; then
if [ "$is_auth" = true ]; then
should=true
else
title_has_v2=false; echo "$PR_TITLE" | grep -qiE 'v2|version.?2|version.?two' && title_has_v2=true
branch_has_kw=false; echo "$PR_BRANCH" | grep -qiE 'v2|react' && branch_has_kw=true
if [ "$is_auth" = true ] && { [ "$title_has_v2" = true ] || [ "$branch_has_kw" = true ]; }; then
should=true
fi
fi
fi
@@ -139,7 +128,7 @@ jobs:
- name: Add deployment started comment
id: deployment-started
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
github-token: ${{ steps.setup-bot.outputs.token }}
script: |
@@ -174,7 +163,7 @@ jobs:
owner,
repo,
issue_number: prNumber,
body: `🚀 **Auto-deploying V2 version** for PR #${prNumber}...\n\n_This is an automated deployment triggered by V2/version2 keywords in the PR title or V2/React keywords in the branch name._\n\n⚠️ **Note:** If new commits are pushed during deployment, this build will be cancelled and replaced with the latest version.`
body: `🚀 **Auto-deploying V2 version** for PR #${prNumber}...\n\n_This is an automated deployment for approved V2 contributors._\n\n⚠️ **Note:** If new commits are pushed during deployment, this build will be cancelled and replaced with the latest version.`
});
return newComment.id;
@@ -306,8 +295,10 @@ jobs:
- /stirling/V2-PR-${{ needs.check-pr.outputs.pr_number }}/config:/configs:rw
- /stirling/V2-PR-${{ needs.check-pr.outputs.pr_number }}/logs:/logs:rw
environment:
DISABLE_ADDITIONAL_FEATURES: "true"
SECURITY_ENABLELOGIN: "false"
DISABLE_ADDITIONAL_FEATURES: "false"
SECURITY_ENABLELOGIN: "true"
SECURITY_INITIALLOGIN_USERNAME: "${{ secrets.TEST_LOGIN_USERNAME }}"
SECURITY_INITIALLOGIN_PASSWORD: "${{ secrets.TEST_LOGIN_PASSWORD }}"
SYSTEM_DEFAULTLOCALE: en-GB
UI_APPNAME: "Stirling-PDF V2 PR#${{ needs.check-pr.outputs.pr_number }}"
UI_HOMEDESCRIPTION: "V2 PR#${{ needs.check-pr.outputs.pr_number }} - Frontend/Backend Split Architecture"
@@ -361,7 +352,7 @@ jobs:
- name: Post V2 deployment URL to PR
if: success()
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
github-token: ${{ steps.setup-bot.outputs.token }}
script: |
@@ -392,7 +383,7 @@ jobs:
`🔗 **Direct Test URL (non-SSL)** [${deploymentUrl}](${deploymentUrl})\n\n` +
`🔐 **Secure HTTPS URL**: [${httpsUrl}](${httpsUrl})\n\n` +
`_This deployment will be automatically cleaned up when the PR is closed._\n\n` +
`🔄 **Auto-deployed** because PR title or branch name contains V2/version2/React keywords.`;
`🔄 **Auto-deployed** for approved V2 contributors.`;
await github.rest.issues.createComment({
owner,
@@ -428,7 +419,7 @@ jobs:
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- name: Clean up V2 deployment comments
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
github-token: ${{ steps.setup-bot.outputs.token }}
script: |
@@ -14,6 +14,7 @@ jobs:
permissions:
issues: write
if: |
vars.CI_PROFILE != 'lite' &&
github.event.issue.pull_request &&
(
contains(github.event.comment.body, 'prdeploy') ||
@@ -40,12 +41,12 @@ jobs:
enable_enterprise: ${{ steps.check-pro-flag.outputs.enable_enterprise }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
with:
egress-policy: audit
- name: Checkout PR
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Setup GitHub App Bot
if: github.actor != 'dependabot[bot]'
@@ -128,12 +129,12 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
with:
egress-policy: audit
- name: Checkout PR
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Setup GitHub App Bot
if: github.actor != 'dependabot[bot]'
@@ -145,7 +146,7 @@ jobs:
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- name: Checkout PR
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
ref: refs/pull/${{ needs.check-comment.outputs.pr_number }}/merge
token: ${{ steps.setup-bot.outputs.token }}
@@ -180,7 +181,7 @@ jobs:
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
context: .
file: ./Dockerfile
file: ./docker/embedded/Dockerfile
push: true
tags: ${{ secrets.DOCKER_HUB_USERNAME }}/test:pr-${{ needs.check-comment.outputs.pr_number }}
build-args: VERSION_TAG=alpha
@@ -356,3 +357,149 @@ jobs:
rm -f ../private.key docker-compose.yml
echo "Cleanup complete."
continue-on-error: true
handle-label-commands:
if: ${{ github.event.issue.pull_request != null }}
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
with:
egress-policy: audit
- name: Check out the repository
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Setup GitHub App Bot
id: setup-bot
uses: ./.github/actions/setup-bot
with:
app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- name: Apply label commands
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
github-token: ${{ steps.setup-bot.outputs.token }}
script: |
const fs = require('fs');
const path = require('path');
const { comment, issue } = context.payload;
const commentBody = comment?.body ?? '';
if (!commentBody.includes('::label::')) {
core.info('No label commands detected in comment.');
return;
}
const configPath = path.join(process.env.GITHUB_WORKSPACE, '.github', 'config', 'repo_devs.json');
const repoDevsConfig = JSON.parse(fs.readFileSync(configPath, 'utf8'));
const label_changer = (repoDevsConfig.label_changer || []).map((login) => login.toLowerCase());
const commenter = (comment?.user?.login || '').toLowerCase();
if (!label_changer.includes(commenter)) {
core.info(`User ${commenter} is not authorized to manage labels.`);
return;
}
const labelsConfigPath = path.join(process.env.GITHUB_WORKSPACE, '.github', 'labels.yml');
const labelsFile = fs.readFileSync(labelsConfigPath, 'utf8');
const labelNameMap = new Map();
for (const match of labelsFile.matchAll(/-\s+name:\s*(?:"([^"]+)"|'([^']+)'|([^\n]+))/g)) {
const labelName = (match[1] ?? match[2] ?? match[3] ?? '').trim();
if (!labelName) {
continue;
}
const normalized = labelName.toLowerCase();
if (!labelNameMap.has(normalized)) {
labelNameMap.set(normalized, labelName);
}
}
if (!labelNameMap.size) {
core.warning('No labels could be read from .github/labels.yml; aborting label commands.');
return;
}
let allowedLabelNames = new Set(labelNameMap.values());
const labelsToAdd = new Set();
const labelsToRemove = new Set();
const commandRegex = /^(\w+)::(label)::"([^"]+)"/gim;
let match;
while ((match = commandRegex.exec(commentBody)) !== null) {
core.info(`Found label command: ${match[0]} (action: ${match[1]}, label: ${match[2]}, labelName: ${match[3]})`);
const action = match[1].toLowerCase();
const labelName = match[3].trim();
if (!labelName) {
continue;
}
const normalized = labelName.toLowerCase();
const resolvedLabelName = labelNameMap.get(normalized);
if (action === 'add') {
if (!resolvedLabelName) {
core.warning(`Label "${labelName}" is not defined in .github/labels.yml and cannot be added.`);
continue;
}
if (!allowedLabelNames.has(resolvedLabelName)) {
core.warning(`Label "${resolvedLabelName}" is not allowed for add commands and will be skipped.`);
continue;
}
labelsToAdd.add(resolvedLabelName);
} else if (action === 'rm') {
const labelToRemove = resolvedLabelName ?? labelName;
if (!resolvedLabelName) {
core.warning(`Label "${labelName}" is not defined in .github/labels.yml; attempting to remove as provided.`);
}
labelsToRemove.add(labelToRemove);
}
}
const addLabels = Array.from(labelsToAdd);
const removeLabels = Array.from(labelsToRemove);
if (!addLabels.length && !removeLabels.length) {
core.info('No valid label commands found after parsing.');
return;
}
const issueParams = {
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
};
if (addLabels.length) {
core.info(`Adding labels: ${addLabels.join(', ')}`);
await github.rest.issues.addLabels({
...issueParams,
labels: addLabels,
});
}
for (const labelName of removeLabels) {
core.info(`Removing label: ${labelName}`);
try {
await github.rest.issues.removeLabel({
...issueParams,
name: labelName,
});
} catch (error) {
if (error.status === 404) {
core.warning(`Label "${labelName}" was not present on the pull request.`);
} else {
throw error;
}
}
}
await github.rest.issues.deleteComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: comment.id,
});
core.info('Processed label commands and deleted the comment.');
+2 -2
View File
@@ -21,12 +21,12 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
with:
egress-policy: audit
- name: Checkout PR
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Setup GitHub App Bot
if: github.actor != 'dependabot[bot]'
+3 -3
View File
@@ -19,11 +19,11 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
with:
egress-policy: audit
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
fetch-depth: 0
@@ -87,7 +87,7 @@ jobs:
- name: AI PR Title Analysis
if: steps.actor.outputs.is_repo_dev == 'true'
id: ai-title-analysis
uses: actions/ai-inference@b81b2afb8390ee6839b494a404766bef6493c7d9 # v1.2.8
uses: actions/ai-inference@334892bb203895caaed82ec52d23c1ed9385151e # v2.0.4
with:
model: openai/gpt-4o
system-prompt-file: ".github/config/system-prompt.txt"
+5 -2
View File
@@ -2,6 +2,9 @@ name: "Auto Pull Request Labeler V2"
on:
pull_request_target:
types: [opened, synchronize]
branches:
- main
- V2
permissions:
contents: read
@@ -13,11 +16,11 @@ jobs:
pull-requests: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
with:
egress-policy: audit
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Setup GitHub App Bot
id: setup-bot
+17 -4
View File
@@ -238,7 +238,7 @@ jobs:
sudo chmod +x /usr/local/bin/docker-compose
- name: Set up Python
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with:
python-version: "3.12"
cache: 'pip' # caching pip dependencies
@@ -262,7 +262,13 @@ jobs:
strategy:
fail-fast: false
matrix:
docker-rev: ["Dockerfile", "Dockerfile.ultra-lite", "Dockerfile.fat"]
include:
- docker-rev: docker/embedded/Dockerfile
artifact-suffix: Dockerfile
- docker-rev: docker/embedded/Dockerfile.ultra-lite
artifact-suffix: Dockerfile.ultra-lite
- docker-rev: docker/embedded/Dockerfile.fat
artifact-suffix: Dockerfile.fat
steps:
- name: Harden Runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
@@ -272,6 +278,13 @@ jobs:
- name: Checkout Repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Free disk space on runner
run: |
echo "Disk space before cleanup:" && df -h
sudo rm -rf /usr/share/dotnet /opt/ghc /usr/local/lib/android /usr/local/share/boost
docker system prune -af || true
echo "Disk space after cleanup:" && df -h
- name: Set up JDK 17
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
with:
@@ -301,7 +314,7 @@ jobs:
with:
builder: ${{ steps.buildx.outputs.name }}
context: .
file: ./docker/backend/${{ matrix.docker-rev }}
file: ./${{ matrix.docker-rev }}
push: false
cache-from: type=gha
cache-to: type=gha,mode=max
@@ -313,7 +326,7 @@ jobs:
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: reports-docker-${{ matrix.docker-rev }}
name: reports-docker-${{ matrix.artifact-suffix }}
path: |
build/reports/tests/
build/test-results/
@@ -1,19 +1,14 @@
name: Check Properties Files on PR
name: Check TOML Translation Files on PR
# This workflow validates TOML translation files
on:
pull_request_target:
types: [opened, synchronize, reopened]
paths:
- "app/core/src/main/resources/messages_*.properties"
- "frontend/public/locales/*/translation.toml"
# cancel in-progress jobs if a new job is triggered
# This is useful to avoid running multiple builds for the same branch if a new commit is pushed
# or a pull request is updated.
# It helps to save resources and time by ensuring that only the latest commit is built and tested
# This is particularly useful for long-running jobs that may take a while to complete.
# The `group` is set to a combination of the workflow name, event name, and branch name.
# This ensures that jobs are grouped by the workflow and branch, allowing for cancellation of
# in-progress jobs when a new commit is pushed to the same branch or a new pull request is opened.
concurrency:
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref_name || github.ref }}
cancel-in-progress: true
@@ -30,12 +25,12 @@ jobs:
pull-requests: write # Allow writing to pull requests
steps:
- name: Harden Runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
with:
egress-policy: audit
- name: Checkout main branch first
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Setup GitHub App Bot
id: setup-bot
@@ -73,22 +68,22 @@ jobs:
run: |
echo "Fetching PR changed files..."
echo "Getting list of changed files from PR..."
# Check if PR number exists
if [ -z "${{ steps.get-pr-data.outputs.pr_number }}" ]; then
echo "Error: PR number is empty"
exit 1
fi
# Get changed files and filter for properties files, handle case where no matches are found
gh pr view ${{ steps.get-pr-data.outputs.pr_number }} --json files -q ".files[].path" | grep -E '^app/core/src/main/resources/messages_[a-zA-Z_]{2}_[a-zA-Z_]{2,7}\.properties$' > changed_files.txt || echo "No matching properties files found in PR"
# Check if any files were found
if [ ! -s changed_files.txt ]; then
echo "No properties files changed in this PR"
echo "Workflow will exit early as no relevant files to check"
exit 0
fi
echo "Found $(wc -l < changed_files.txt) matching properties files"
# Check if PR number exists
if [ -z "${{ steps.get-pr-data.outputs.pr_number }}" ]; then
echo "Error: PR number is empty"
exit 1
fi
# Get changed files and filter for TOML translation files
gh pr view ${{ steps.get-pr-data.outputs.pr_number }} --json files -q ".files[].path" | grep -E '^frontend/public/locales/[a-zA-Z-]+/translation\.toml$' > changed_files.txt || echo "No matching TOML files found in PR"
# Check if any files were found
if [ ! -s changed_files.txt ]; then
echo "No TOML translation files changed in this PR"
echo "Workflow will exit early as no relevant files to check"
exit 0
fi
echo "Found $(wc -l < changed_files.txt) matching TOML files"
- name: Determine reference file test
- name: Determine reference file
id: determine-file
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
@@ -125,11 +120,11 @@ jobs:
pull_number: prNumber,
});
// Filter for relevant files based on the PR changes
// Filter for relevant TOML files based on the PR changes
const changedFiles = files
.filter(file =>
file.status !== "removed" &&
/^app\/core\/src\/main\/resources\/messages_[a-zA-Z_]{2}_[a-zA-Z_]{2,7}\.properties$/.test(file.filename)
/^frontend\/public\/locales\/[a-zA-Z-]+\/translation\.toml$/.test(file.filename)
)
.map(file => file.filename);
@@ -169,16 +164,16 @@ jobs:
// Determine reference file
let referenceFilePath;
if (changedFiles.includes("app/core/src/main/resources/messages_en_GB.properties")) {
if (changedFiles.includes("frontend/public/locales/en-GB/translation.toml")) {
console.log("Using PR branch reference file.");
const { data: fileContent } = await github.rest.repos.getContent({
owner: prRepoOwner,
repo: prRepoName,
path: "app/core/src/main/resources/messages_en_GB.properties",
path: "frontend/public/locales/en-GB/translation.toml",
ref: branch,
});
referenceFilePath = "pr-branch-messages_en_GB.properties";
referenceFilePath = "pr-branch-translation-en-GB.toml";
const content = Buffer.from(fileContent.content, "base64").toString("utf-8");
fs.writeFileSync(referenceFilePath, content);
} else {
@@ -186,11 +181,11 @@ jobs:
const { data: fileContent } = await github.rest.repos.getContent({
owner: repoOwner,
repo: repoName,
path: "app/core/src/main/resources/messages_en_GB.properties",
path: "frontend/public/locales/en-GB/translation.toml",
ref: "main",
});
referenceFilePath = "main-branch-messages_en_GB.properties";
referenceFilePath = "main-branch-translation-en-GB.toml";
const content = Buffer.from(fileContent.content, "base64").toString("utf-8");
fs.writeFileSync(referenceFilePath, content);
}
@@ -198,11 +193,20 @@ jobs:
console.log(`Reference file path: ${referenceFilePath}`);
core.exportVariable("REFERENCE_FILE", referenceFilePath);
- name: Set up Python
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with:
python-version: "3.12"
- name: Install Python dependencies
run: |
pip install tomli-w
- name: Run Python script to check files
id: run-check
run: |
echo "Running Python script to check files..."
python .github/scripts/check_language_properties.py \
echo "Running Python script to check TOML files..."
python .github/scripts/check_language_toml.py \
--actor ${{ github.event.pull_request.user.login }} \
--reference-file "${REFERENCE_FILE}" \
--branch "pr-branch" \
@@ -213,7 +217,7 @@ jobs:
id: capture-output
run: |
if [ -f result.txt ] && [ -s result.txt ]; then
echo "Test, capturing output..."
echo "Capturing output..."
SCRIPT_OUTPUT=$(cat result.txt)
echo "SCRIPT_OUTPUT<<EOF" >> $GITHUB_ENV
echo "$SCRIPT_OUTPUT" >> $GITHUB_ENV
@@ -227,7 +231,7 @@ jobs:
echo "FAIL_JOB=false" >> $GITHUB_ENV
fi
else
echo "No update found."
echo "No output found."
echo "SCRIPT_OUTPUT=" >> $GITHUB_ENV
echo "FAIL_JOB=false" >> $GITHUB_ENV
fi
@@ -249,7 +253,7 @@ jobs:
issue_number: issueNumber
});
const comment = comments.data.find(c => c.body.includes("## 🚀 Translation Verification Summary"));
const comment = comments.data.find(c => c.body.includes("## 🌐 TOML Translation Verification Summary"));
// Only update or create comments by the action user
const expectedActor = "${{ steps.setup-bot.outputs.app-slug }}[bot]";
@@ -260,7 +264,7 @@ jobs:
owner: repoOwner,
repo: repoName,
comment_id: comment.id,
body: `## 🚀 Translation Verification Summary\n\n\n${SCRIPT_OUTPUT}\n`
body: `## 🌐 TOML Translation Verification Summary\n\n\n${SCRIPT_OUTPUT}\n`
});
console.log("Updated existing comment.");
} else if (!comment) {
@@ -269,7 +273,7 @@ jobs:
owner: repoOwner,
repo: repoName,
issue_number: issueNumber,
body: `## 🚀 Translation Verification Summary\n\n\n${SCRIPT_OUTPUT}\n`
body: `## 🌐 TOML Translation Verification Summary\n\n\n${SCRIPT_OUTPUT}\n`
});
console.log("Created new comment.");
} else {
@@ -287,6 +291,6 @@ jobs:
run: |
echo "Cleaning up temporary files..."
rm -rf pr-branch
rm -f pr-branch-messages_en_GB.properties main-branch-messages_en_GB.properties changed_files.txt result.txt
rm -f pr-branch-translation-en-GB.toml main-branch-translation-en-GB.toml changed_files.txt result.txt
echo "Cleanup complete."
continue-on-error: true # Ensure cleanup runs even if previous steps fail
+3 -3
View File
@@ -17,13 +17,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
with:
egress-policy: audit
- name: "Checkout Repository"
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: "Dependency Review"
uses: actions/dependency-review-action@56339e523c0409420f6c2c9a2f4292bbb3c07dd3 # v4.8.0
uses: actions/dependency-review-action@3c4e3dcb1aa7874d2c16be7d79418e9b7efd6261 # v4.8.2
with:
config-file: './.github/config/dependency-review-config.yml'
@@ -253,7 +253,7 @@ jobs:
- name: Create Pull Request (Push only)
id: cpr
if: github.event_name == 'push' && env.CHANGES_DETECTED == 'true'
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
with:
token: ${{ steps.setup-bot.outputs.token }}
commit-message: "Update Frontend 3rd Party Licenses"
+4 -4
View File
@@ -31,12 +31,12 @@ jobs:
repository-projects: write # Required for enabling automerge
steps:
- name: Harden Runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
with:
egress-policy: audit
- name: Check out code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
fetch-depth: 0
@@ -64,7 +64,7 @@ jobs:
- name: Upload artifact on failure
if: failure()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: dependencies-without-allowed-license.json
path: build/reports/dependency-license/dependencies-without-allowed-license.json
@@ -82,7 +82,7 @@ jobs:
- name: Create Pull Request
id: cpr
if: env.CHANGES_DETECTED == 'true'
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
with:
token: ${{ steps.setup-bot.outputs.token }}
commit-message: "Update 3rd Party Licenses"
+2 -2
View File
@@ -15,12 +15,12 @@ jobs:
issues: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
with:
egress-policy: audit
- name: Check out the repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Run Labeler
uses: crazy-max/ghaction-github-labeler@24d110aa46a59976b8a7f35518cb7f14f434c916 # v5.3.0
+511 -253
View File
@@ -1,4 +1,4 @@
name: Test Installers Build
name: Multi-OS Tauri Releases
on:
workflow_dispatch:
@@ -6,7 +6,23 @@ on:
test_mode:
description: "Run in test mode (skip release step)"
required: false
default: "false"
default: "true"
type: choice
options:
- "true"
- "false"
platform:
description: "Platform to build (windows, macos, linux, or all)"
required: true
default: "all"
type: choice
options:
- all
- windows
- macos
- linux
push:
branches: [main, V2, V2-demo, V2-master]
release:
types: [created]
@@ -14,305 +30,547 @@ permissions:
contents: read
jobs:
read_versions:
determine-matrix:
if: ${{ vars.CI_PROFILE != 'lite' }}
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
version: ${{ steps.versionNumber.outputs.versionNumber }}
versionMac: ${{ steps.versionNumberMac.outputs.versionNumberMac }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
with:
egress-policy: audit
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Set up JDK
- name: Set up JDK 21
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
with:
distribution: 'temurin'
java-version: '21'
java-version: "21"
distribution: "temurin"
- uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
with:
gradle-version: 8.14
# ✅ Get version from Gradle
- name: Get version number
id: versionNumber
run: |
echo "Running gradlew printVersion..."
./gradlew printVersion --quiet
VERSION=$(./gradlew printVersion --quiet | tail -1)
echo "Extracted version: $VERSION"
echo "versionNumber=$VERSION" >> $GITHUB_OUTPUT
# ✅ Get Mac-specific version from Gradle
- name: Get version number mac
id: versionNumberMac
- name: Determine build matrix
id: set-matrix
run: |
VERSION_MAC=$(./gradlew printMacVersion --quiet | tail -1)
echo "versionNumberMac=$VERSION_MAC" >> $GITHUB_OUTPUT
build-portable:
needs: read_versions
runs-on: ubuntu-latest
strategy:
matrix:
disable_security: [true, false]
include:
- disable_security: false
file_suffix: "-with-login"
- disable_security: true
file_suffix: ""
steps:
- name: Harden Runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
with:
egress-policy: audit
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up JDK 21
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
with:
java-version: "21"
distribution: "temurin"
- uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
with:
gradle-version: 8.14
- name: Generate jar (Disable Security=${{ matrix.disable_security }})
run: ./gradlew clean createExe
env:
DISABLE_ADDITIONAL_FEATURES: ${{ matrix.disable_security }}
STIRLING_PDF_DESKTOP_UI: false
- name: Rename binaries
run: |
mkdir ./binaries
mv ./build/launch4j/Stirling-PDF.exe ./binaries/win-Stirling-PDF-portable-Server${{ matrix.file_suffix }}.exe
mv ./app/core/build/libs/stirling-pdf-${{ needs.read_versions.outputs.version }}.jar ./binaries/Stirling-PDF${{ matrix.file_suffix }}.jar
- name: Upload build artifacts
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
retention-days: 1
if-no-files-found: error
name: stirling${{ matrix.file_suffix }}-binaries
path: |
./binaries/*
sign_verify-portable:
needs: [build-portable, read_versions]
runs-on: ubuntu-latest
strategy:
matrix:
disable_security: [true, false]
include:
- disable_security: false
file_suffix: "with-login-"
- disable_security: true
file_suffix: ""
steps:
- name: Harden Runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
with:
egress-policy: audit
- name: Download build artifacts
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
with:
name: stirling-${{ matrix.file_suffix }}binaries
- name: Display structure of downloaded files
run: ls -R
- name: Upload signed artifacts
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
retention-days: 1
if-no-files-found: error
name: stirling-${{ matrix.file_suffix }}signed
path: |
./*
!cosign.*
build-installers:
needs: read_versions
strategy:
matrix:
include:
- os: windows-latest
platform: win-
- os: macos-latest
platform: mac-
# - os: ubuntu-latest
# platform: linux-
runs-on: ${{ matrix.os }}
permissions:
contents: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
with:
egress-policy: audit
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up JDK 21
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
with:
java-version: "21"
distribution: "temurin"
- uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
with:
gradle-version: 8.14
# Install Windows dependencies
- name: Install WiX Toolset
if: matrix.os == 'windows-latest'
run: |
curl -L -o wix.exe https://github.com/wixtoolset/wix3/releases/download/wix3141rtm/wix314.exe
.\wix.exe /install /quiet
# Build installer
- name: Build Installer
run: ./gradlew build jpackage -x test --info
env:
DISABLE_ADDITIONAL_FEATURES: true
STIRLING_PDF_DESKTOP_UI: true
BROWSER_OPEN: true
- name: Set up JDK (x86_64)
if: matrix.os == 'macos-latest'
run: |
curl -L -o jdk.tar.gz https://cdn.azul.com/zulu/bin/zulu17.56.15-ca-jdk17.0.14-macosx_x64.tar.gz
mkdir -p zulu17
tar -xzf jdk.tar.gz -C zulu17 --strip-components=1
echo "JAVA_HOME=$PWD/zulu17" >> $GITHUB_ENV
echo "$PWD/zulu17/bin" >> $GITHUB_PATH
- name: Verify JDK architecture
if: matrix.os == 'macos-latest'
run: file $JAVA_HOME/bin/java
- name: Build project and run jpackage (x86_64)
if: matrix.os == 'macos-latest'
run: arch -x86_64 ./gradlew jpackageMacX64
# Rename and collect artifacts based on OS
- name: Prepare artifacts
id: prepare
shell: bash
run: |
ls -lah ./build/jpackage/
mkdir ./binaries
if [ "${{ matrix.os }}" = "windows-latest" ]; then
mv "./build/jpackage/Stirling PDF-${{ needs.read_versions.outputs.version }}.exe" "./binaries/Stirling-PDF-win-installer.exe"
elif [ "${{ matrix.os }}" = "macos-latest" ]; then
mv "./build/jpackage/Stirling PDF-${{ needs.read_versions.outputs.versionMac }}.dmg" "./binaries/Stirling-PDF-mac-installer.dmg"
mv "./build/jpackage/x86_64/Stirling PDF (x86_64)-${{ needs.read_versions.outputs.versionMac }}.dmg" "./binaries/Stirling-PDF-mac-x86_64-installer.dmg"
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
case "${{ github.event.inputs.platform }}" in
"windows")
echo 'matrix={"include":[{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64"}]}' >> $GITHUB_OUTPUT
;;
"macos")
echo 'matrix={"include":[{"platform":"macos-15","args":"--target aarch64-apple-darwin","name":"macos-aarch64"},{"platform":"macos-15-intel","args":"--target x86_64-apple-darwin","name":"macos-x86_64"}]}' >> $GITHUB_OUTPUT
;;
"linux")
echo 'matrix={"include":[{"platform":"ubuntu-22.04","args":"","name":"linux-x86_64"}]}' >> $GITHUB_OUTPUT
;;
*)
echo 'matrix={"include":[{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64"},{"platform":"macos-15","args":"--target aarch64-apple-darwin","name":"macos-aarch64"},{"platform":"macos-15-intel","args":"--target x86_64-apple-darwin","name":"macos-x86_64"},{"platform":"ubuntu-22.04","args":"","name":"linux-x86_64"}]}' >> $GITHUB_OUTPUT
;;
esac
else
mv "./build/jpackage/stirling-pdf_${{ needs.read_versions.outputs.version }}-1_amd64.deb" "./binaries/Stirling-PDF-linux-installer.deb"
# For push/release events, build all platforms
echo 'matrix={"include":[{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64"},{"platform":"macos-15","args":"--target aarch64-apple-darwin","name":"macos-aarch64"},{"platform":"macos-15-intel","args":"--target x86_64-apple-darwin","name":"macos-x86_64"},{"platform":"ubuntu-22.04","args":"","name":"linux-x86_64"}]}' >> $GITHUB_OUTPUT
fi
- name: Display structure of downloaded files
run: ls -R ./binaries
- name: Upload build artifacts
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
retention-days: 1
if-no-files-found: error
name: ${{ matrix.platform }}binaries
path: |
./binaries/*
sign_verify:
needs: [read_versions, build-installers]
build-jars:
needs: determine-matrix
runs-on: ubuntu-latest
strategy:
matrix:
include:
- os: windows-latest
platform: win-
- os: macos-latest
platform: mac-
# - os: ubuntu-latest
# platform: linux-
runs-on: ubuntu-latest
variant:
- name: "default"
disable_security: true
build_frontend: true
file_suffix: ""
- name: "with-login"
disable_security: false
build_frontend: true
file_suffix: "-with-login"
- name: "server-only"
disable_security: true
build_frontend: false
file_suffix: "-server"
steps:
- name: Harden Runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
with:
egress-policy: audit
- name: Download build artifacts
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Set up JDK 21
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
with:
name: ${{ matrix.platform }}binaries
java-version: "21"
distribution: "temurin"
- name: Display structure of downloaded files
run: ls -R
- uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
with:
gradle-version: 8.14
- name: Install Cosign
if: matrix.os == 'windows-latest'
uses: sigstore/cosign-installer@d7543c93d881b35a8faa02e8e3605f69b7a1ce62 # v3.10.0
- name: Setup Node.js
if: matrix.variant.build_frontend == true
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
cache-dependency-path: frontend/package-lock.json
- name: Generate key pair
if: matrix.os == 'windows-latest'
run: cosign generate-key-pair
- name: Build JAR
run: ./gradlew clean build ${{ matrix.variant.build_frontend && '-PbuildWithFrontend=true' || '' }} -x spotlessApply -x spotlessCheck -x test -x sonarqube
env:
DISABLE_ADDITIONAL_FEATURES: ${{ matrix.variant.disable_security }}
STIRLING_PDF_DESKTOP_UI: false
- name: Sign and generate attestations
if: matrix.os == 'windows-latest'
- name: Rename JAR
run: |
cosign sign-blob \
--key ./cosign.key \
--yes \
--output-signature ./Stirling-PDF-win-installer.exe.sig \
./Stirling-PDF-win-installer.exe
echo "Version from determine-matrix: ${{ needs.determine-matrix.outputs.version }}"
echo "Looking for: app/core/build/libs/stirling-pdf-${{ needs.determine-matrix.outputs.version }}.jar"
ls -la app/core/build/libs/
mkdir -p ./jar-dist
cp app/core/build/libs/stirling-pdf-${{ needs.determine-matrix.outputs.version }}.jar ./jar-dist/Stirling-PDF${{ matrix.variant.file_suffix }}.jar
cosign attest-blob \
--predicate - \
--key ./cosign.key \
--yes \
--output-attestation ./Stirling-PDF-win-installer.exe.intoto.jsonl \
./Stirling-PDF-win-installer.exe
cosign verify-blob \
--key ./cosign.pub \
--signature ./Stirling-PDF-win-installer.exe.sig \
./Stirling-PDF-win-installer.exe
- name: Display structure of downloaded files
run: ls -R
- name: Upload signed artifacts
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
- name: Upload JAR artifacts
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: jar${{ matrix.variant.file_suffix }}
path: ./jar-dist/*.jar
retention-days: 1
build:
needs: determine-matrix
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.determine-matrix.outputs.matrix) }}
runs-on: ${{ matrix.platform }}
env:
SM_API_KEY: ${{ secrets.SM_API_KEY }}
WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
with:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Install dependencies (ubuntu only)
if: matrix.platform == 'ubuntu-22.04'
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libjavascriptcoregtk-4.0-dev libsoup2.4-dev libjavascriptcoregtk-4.1-dev libsoup-3.0-dev
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
cache-dependency-path: frontend/package-lock.json
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
with:
toolchain: stable
targets: ${{ (matrix.platform == 'macos-15' || matrix.platform == 'macos-15-intel') && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
- name: Set up JDK 21
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
with:
java-version: "21"
distribution: "temurin"
- name: Build Java backend with JLink
working-directory: ./
shell: bash
run: |
chmod +x ./gradlew
echo "🔧 Building Stirling-PDF JAR..."
./gradlew clean build -x spotlessApply -x spotlessCheck -x test -x sonarqube
# Find the built JAR
STIRLING_JAR=$(ls app/core/build/libs/stirling-pdf-*.jar | head -n 1)
echo "✅ Built JAR: $STIRLING_JAR"
# Create Tauri directories
mkdir -p ./frontend/src-tauri/libs
mkdir -p ./frontend/src-tauri/runtime
# Copy JAR to Tauri libs
cp "$STIRLING_JAR" ./frontend/src-tauri/libs/
echo "✅ JAR copied to Tauri libs"
# Analyze JAR dependencies for jlink modules
echo "🔍 Analyzing JAR dependencies..."
if command -v jdeps &> /dev/null; then
DETECTED_MODULES=$(jdeps --print-module-deps --ignore-missing-deps "$STIRLING_JAR" 2>/dev/null || echo "")
if [ -n "$DETECTED_MODULES" ]; then
echo "📋 jdeps detected modules: $DETECTED_MODULES"
MODULES="$DETECTED_MODULES,java.compiler,java.instrument,java.management,java.naming,java.net.http,java.prefs,java.rmi,java.scripting,java.security.jgss,java.security.sasl,java.sql,java.transaction.xa,java.xml.crypto,jdk.crypto.ec,jdk.crypto.cryptoki,jdk.unsupported"
else
echo "⚠️ jdeps analysis failed, using predefined modules"
MODULES="java.base,java.compiler,java.desktop,java.instrument,java.logging,java.management,java.naming,java.net.http,java.prefs,java.rmi,java.scripting,java.security.jgss,java.security.sasl,java.sql,java.transaction.xa,java.xml,java.xml.crypto,jdk.crypto.ec,jdk.crypto.cryptoki,jdk.unsupported"
fi
else
echo "⚠️ jdeps not available, using predefined modules"
MODULES="java.base,java.compiler,java.desktop,java.instrument,java.logging,java.management,java.naming,java.net.http,java.prefs,java.rmi,java.scripting,java.security.jgss,java.security.sasl,java.sql,java.transaction.xa,java.xml,java.xml.crypto,jdk.crypto.ec,jdk.crypto.cryptoki,jdk.unsupported"
fi
# Create custom JRE with jlink
echo "🔧 Creating custom JRE with jlink..."
echo "📋 Using modules: $MODULES"
# Remove any existing JRE
rm -rf ./frontend/src-tauri/runtime/jre
# Create the custom JRE
jlink \
--add-modules "$MODULES" \
--strip-debug \
--compress=2 \
--no-header-files \
--no-man-pages \
--output ./frontend/src-tauri/runtime/jre
if [ ! -d "./frontend/src-tauri/runtime/jre" ]; then
echo "❌ Failed to create JLink runtime"
exit 1
fi
# Test the bundled runtime
if [ -f "./frontend/src-tauri/runtime/jre/bin/java" ]; then
RUNTIME_VERSION=$(./frontend/src-tauri/runtime/jre/bin/java --version 2>&1 | head -n 1)
echo "✅ Custom JRE created successfully: $RUNTIME_VERSION"
else
echo "❌ Custom JRE executable not found"
exit 1
fi
# Calculate runtime size
RUNTIME_SIZE=$(du -sh ./frontend/src-tauri/runtime/jre | cut -f1)
echo "📊 Custom JRE size: $RUNTIME_SIZE"
env:
DISABLE_ADDITIONAL_FEATURES: true
- name: Install frontend dependencies
working-directory: ./frontend
run: npm install
# DigiCert KeyLocker Setup (Cloud HSM)
- name: Setup DigiCert KeyLocker
id: digicert-setup
if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && (github.event_name == 'release' || github.ref == 'refs/heads/V2-master') }}
uses: digicert/ssm-code-signing@v1.1.0
env:
SM_API_KEY: ${{ secrets.SM_API_KEY }}
SM_CLIENT_CERT_FILE_B64: ${{ secrets.SM_CLIENT_CERT_FILE_B64 }}
SM_CLIENT_CERT_PASSWORD: ${{ secrets.SM_CLIENT_CERT_PASSWORD }}
SM_KEYPAIR_ALIAS: ${{ secrets.SM_KEYPAIR_ALIAS }}
SM_HOST: ${{ secrets.SM_HOST }}
- name: Setup DigiCert KeyLocker Certificate
if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && (github.event_name == 'release' || github.ref == 'refs/heads/V2-master') }}
shell: pwsh
run: |
Write-Host "Setting up DigiCert KeyLocker environment..."
# Decode client certificate
$certBytes = [Convert]::FromBase64String("${{ secrets.SM_CLIENT_CERT_FILE_B64 }}")
$certPath = "D:\Certificate_pkcs12.p12"
[IO.File]::WriteAllBytes($certPath, $certBytes)
# Set environment variables
echo "SM_CLIENT_CERT_FILE=D:\Certificate_pkcs12.p12" >> $env:GITHUB_ENV
echo "SM_HOST=${{ secrets.SM_HOST }}" >> $env:GITHUB_ENV
echo "SM_API_KEY=${{ secrets.SM_API_KEY }}" >> $env:GITHUB_ENV
echo "SM_CLIENT_CERT_PASSWORD=${{ secrets.SM_CLIENT_CERT_PASSWORD }}" >> $env:GITHUB_ENV
echo "SM_KEYPAIR_ALIAS=${{ secrets.SM_KEYPAIR_ALIAS }}" >> $env:GITHUB_ENV
# Get PKCS11 config path from DigiCert action
$pkcs11Config = $env:PKCS11_CONFIG
if ($pkcs11Config) {
Write-Host "Found PKCS11_CONFIG: $pkcs11Config"
echo "PKCS11_CONFIG=$pkcs11Config" >> $env:GITHUB_ENV
} else {
Write-Host "PKCS11_CONFIG not set by DigiCert action, using default path"
$defaultPath = "C:\Users\RUNNER~1\AppData\Local\Temp\smtools-windows-x64\pkcs11properties.cfg"
if (Test-Path $defaultPath) {
Write-Host "Found config at default path: $defaultPath"
echo "PKCS11_CONFIG=$defaultPath" >> $env:GITHUB_ENV
} else {
Write-Host "Warning: Could not find PKCS11 config file"
}
}
# Traditional PFX Certificate Import (fallback if KeyLocker not configured)
- name: Import Windows Code Signing Certificate
if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY == '' && (github.event_name == 'release' || github.ref == 'refs/heads/V2-master') }}
env:
WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }}
WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}
shell: powershell
run: |
if ($env:WINDOWS_CERTIFICATE) {
Write-Host "Importing Windows Code Signing Certificate..."
# Decode base64 certificate and save to file
$certBytes = [Convert]::FromBase64String($env:WINDOWS_CERTIFICATE)
$certPath = Join-Path $env:RUNNER_TEMP "certificate.pfx"
[IO.File]::WriteAllBytes($certPath, $certBytes)
# Import certificate to CurrentUser\My store
$cert = Import-PfxCertificate -FilePath $certPath -CertStoreLocation Cert:\CurrentUser\My -Password (ConvertTo-SecureString -String $env:WINDOWS_CERTIFICATE_PASSWORD -AsPlainText -Force)
# Extract and set thumbprint as environment variable
$thumbprint = $cert.Thumbprint
Write-Host "Certificate imported with thumbprint: $thumbprint"
echo "WINDOWS_CERTIFICATE_THUMBPRINT=$thumbprint" >> $env:GITHUB_ENV
# Clean up certificate file
Remove-Item $certPath
Write-Host "Windows certificate import completed."
} else {
Write-Host "⚠️ WINDOWS_CERTIFICATE secret not set - building unsigned binary"
}
- name: Import Apple Developer Certificate
if: (matrix.platform == 'macos-15' || matrix.platform == 'macos-15-intel') && (github.event_name == 'release' || github.ref == 'refs/heads/V2-master')
env:
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
run: |
echo "Importing Apple Developer Certificate..."
echo $APPLE_CERTIFICATE | base64 --decode > certificate.p12
# Create temporary keychain
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
KEYCHAIN_PASSWORD=$(openssl rand -base64 32)
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
# Import certificate
security import certificate.p12 -P "$APPLE_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
security list-keychain -d user -s $KEYCHAIN_PATH
security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
# Clean up
rm certificate.p12
- name: Verify Certificate
if: (matrix.platform == 'macos-15' || matrix.platform == 'macos-15-intel') && (github.event_name == 'release' || github.ref == 'refs/heads/V2-master')
run: |
echo "Verifying Apple Developer Certificate..."
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
CERT_INFO=$(security find-identity -v -p codesigning $KEYCHAIN_PATH | grep "Developer ID Application")
echo "Certificate Info: $CERT_INFO"
CERT_ID=$(echo "$CERT_INFO" | awk -F'"' '{print $2}')
echo "Certificate ID: $CERT_ID"
echo "APPLE_SIGNING_IDENTITY=$CERT_ID" >> $GITHUB_ENV
echo "Certificate imported successfully."
- name: Build Tauri app
uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ env.APPLE_SIGNING_IDENTITY }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPIMAGETOOL_SIGN_PASSPHRASE: ${{ secrets.APPIMAGETOOL_SIGN_PASSPHRASE }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY: ${{ secrets.VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY }}
VITE_SAAS_SERVER_URL: ${{ secrets.VITE_SAAS_SERVER_URL }}
# Only enable Windows signing in Tauri when on release or V2-master
SIGN: ${{ (github.event_name == 'release' || github.ref == 'refs/heads/V2-master') && (env.SM_API_KEY == '' && env.WINDOWS_CERTIFICATE != '') && '1' || '0' }}
CI: true
with:
projectPath: ./frontend
tauriScript: npx tauri
args: ${{ matrix.args }}
# Sign with DigiCert KeyLocker (post-build)
- name: Sign Windows binaries with DigiCert KeyLocker
if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && (github.event_name == 'release' || github.ref == 'refs/heads/V2-master') }}
shell: pwsh
run: |
Write-Host "=== DigiCert KeyLocker Signing ==="
# Test smctl connectivity first
Write-Host "Testing smctl connection..."
$healthCheck = & smctl healthcheck 2>&1
if ($LASTEXITCODE -eq 0) {
Write-Host "[SUCCESS] Connected to DigiCert KeyLocker"
} else {
Write-Host "[ERROR] Failed to connect to DigiCert KeyLocker"
Write-Host $healthCheck
exit 1
}
Write-Host ""
# Sync certificates to Windows certificate store
Write-Host "Syncing certificates to Windows certificate store..."
$syncOutput = & smctl windows certsync 2>&1
Write-Host "Cert sync result: $syncOutput"
Write-Host ""
# Find only the files we need to sign
$filesToSign = @()
# Main application executable
$mainExe = Get-ChildItem -Path "./frontend/src-tauri/target/x86_64-pc-windows-msvc/release" -Filter "stirling-pdf.exe" -File -ErrorAction SilentlyContinue
if ($mainExe) { $filesToSign += $mainExe }
# MSI installer
$msiFiles = Get-ChildItem -Path "./frontend/src-tauri/target" -Filter "*.msi" -Recurse -File
$filesToSign += $msiFiles
if ($filesToSign.Count -eq 0) {
Write-Host "[ERROR] No files found to sign"
exit 1
}
Write-Host "Found $($filesToSign.Count) files to sign:"
foreach ($f in $filesToSign) { Write-Host " - $($f.Name)" }
Write-Host ""
$signedCount = 0
foreach ($file in $filesToSign) {
Write-Host "Signing: $($file.Name)"
# Get PKCS11 config file path
$pkcs11Config = $env:PKCS11_CONFIG
if (-not $pkcs11Config) {
Write-Host "[ERROR] PKCS11_CONFIG environment variable not set"
exit 1
}
Write-Host "Using PKCS11 config: $pkcs11Config"
# Try signing with certificate fingerprint first (if available)
$fingerprint = "${{ secrets.SM_CODE_SIGNING_CERT_SHA1_HASH }}"
if ($fingerprint -and $fingerprint -ne "") {
Write-Host "Attempting to sign with certificate fingerprint..."
$output = & smctl sign --fingerprint "$fingerprint" --input "$($file.FullName)" --config-file "$pkcs11Config" --verbose 2>&1
$exitCode = $LASTEXITCODE
} else {
Write-Host "No fingerprint provided, using keypair alias..."
$output = & smctl sign --keypair-alias "${{ secrets.SM_KEYPAIR_ALIAS }}" --input "$($file.FullName)" --config-file "$pkcs11Config" --verbose 2>&1
$exitCode = $LASTEXITCODE
}
Write-Host "Exit code: $exitCode"
Write-Host "Output: $output"
if ($output -match "FAILED" -or $output -match "error" -or $output -match "Error") {
Write-Host "[ERROR] Signing failed for $($file.Name)"
exit 1
}
if ($exitCode -ne 0) {
Write-Host "[ERROR] Failed to sign $($file.Name)"
Write-Host "Full error output:"
Write-Host $output
exit 1
}
$signedCount++
Write-Host "[SUCCESS] Signed: $($file.Name)"
Write-Host ""
}
Write-Host "=== Summary ==="
Write-Host "[SUCCESS] Signed $signedCount/$($filesToSign.Count) files successfully"
- name: Rename artifacts
shell: bash
run: |
mkdir -p ./dist
cd ./frontend/src-tauri/target
# Find and rename artifacts based on platform
if [ "${{ matrix.platform }}" = "windows-latest" ]; then
find . -name "*.exe" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.exe" \;
find . -name "*.msi" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.msi" \;
elif [ "${{ matrix.platform }}" = "macos-15" ] || [ "${{ matrix.platform }}" = "macos-15-intel" ]; then
find . -name "*.dmg" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.dmg" \;
find . -name "*.app" -exec cp -r {} "../../../dist/Stirling-PDF-${{ matrix.name }}.app" \;
else
find . -name "*.deb" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.deb" \;
find . -name "*.AppImage" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.AppImage" \;
fi
- name: Upload build artifacts
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: Stirling-PDF-${{ matrix.name }}
path: ./dist/*
retention-days: 1
if-no-files-found: error
name: ${{ matrix.platform }}signed
path: |
./Stirling-PDF-${{ matrix.platform }}installer.*
./Stirling-PDF-${{ matrix.platform }}x86_64-installer.*
!cosign.*
create-release:
if: github.event_name != 'workflow_dispatch' || github.event.inputs.test_mode != 'true'
needs: [read_versions, sign_verify, sign_verify-portable]
if: (github.event_name == 'workflow_dispatch' && github.event.inputs.test_mode != 'true') || github.event_name == 'release' || github.ref == 'refs/heads/V2-master'
needs: [determine-matrix, build, build-jars]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
with:
egress-policy: audit
- name: Download signed artifacts
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
- name: Display structure of downloaded files
run: ls -R
- name: Upload binaries, attestations and signatures to Release and create GitHub Release
uses: softprops/action-gh-release@62c96d0c4e8a889135c1f3a25910db8dbe0e85f7 # v2.3.4
- name: Download all Tauri artifacts
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with:
tag_name: v${{ needs.read_versions.outputs.version }}
pattern: Stirling-PDF-*
path: ./artifacts/tauri
- name: Download JAR artifact (default)
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with:
name: jar
path: ./artifacts/jars
- name: Download JAR artifact (with login)
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with:
name: jar-with-login
path: ./artifacts/jars
- name: Download JAR artifact (server only)
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with:
name: jar-server
path: ./artifacts/jars
- name: Display structure of downloaded files
run: ls -R ./artifacts
- name: Upload binaries to Release
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
with:
tag_name: v${{ needs.determine-matrix.outputs.version }}
generate_release_notes: true
files: |
./*signed/*
./artifacts/**/*.jar
./artifacts/**/*.msi
./artifacts/**/*.dmg
./artifacts/**/*.deb
./artifacts/**/*.AppImage
draft: false
prerelease: false
+4 -4
View File
@@ -21,12 +21,12 @@ jobs:
pull-requests: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
with:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
fetch-depth: 0
@@ -38,7 +38,7 @@ jobs:
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- name: Set up Python
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with:
python-version: 3.12
cache: 'pip' # caching pip dependencies
@@ -67,7 +67,7 @@ jobs:
- name: Create Pull Request
if: env.CHANGES_DETECTED == 'true'
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
with:
token: ${{ steps.setup-bot.outputs.token }}
commit-message: ":file_folder: pre-commit"
+246
View File
@@ -0,0 +1,246 @@
name: Push Docker Image - V2 Branch
on:
workflow_dispatch:
push:
branches:
- V2-master
- V1_V2_merge
# cancel in-progress jobs if a new job is triggered
# This is useful to avoid running multiple builds for the same branch if a new commit is pushed
# or a pull request is updated.
# It helps to save resources and time by ensuring that only the latest commit is built and tested
# This is particularly useful for long-running jobs that may take a while to complete.
# The `group` is set to a combination of the workflow name, event name, and branch name.
# This ensures that jobs are grouped by the workflow and branch, allowing for cancellation of
# in-progress jobs when a new commit is pushed to the same branch or a new pull request is opened.
concurrency:
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref_name || github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
push:
if: ${{ vars.CI_PROFILE != 'lite' }}
runs-on: ubuntu-24.04-8core
permissions:
packages: write
id-token: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
with:
egress-policy: audit
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up JDK 21
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
with:
java-version: "21"
distribution: "temurin"
- uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
with:
gradle-version: 8.14
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- name: Get version number
id: versionNumber
run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT
- name: Install cosign
if: github.ref == 'refs/heads/V2-master'
uses: sigstore/cosign-installer@d7543c93d881b35a8faa02e8e3605f69b7a1ce62 # v3.10.0
with:
cosign-release: "v2.4.1"
- name: Login to Docker Hub
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_API }}
- name: Login to GitHub Container Registry
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}
- name: Set up QEMU
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
- name: Convert repository owner to lowercase
id: repoowner
run: echo "lowercase=$(echo ${{ github.repository_owner }} | awk '{print tolower($0)}')" >> $GITHUB_OUTPUT
- name: Generate tags for latest (V2-master branch - production)
id: meta
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
if: github.ref == 'refs/heads/V2-master'
with:
images: |
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/s-pdf
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf
${{ secrets.DOCKER_HUB_ORG_USERNAME }}/stirling-pdf
tags: |
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}
type=raw,value=latest
- name: Generate tags for latest (V1_V2_merge branch - test)
id: meta-test
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
if: github.ref == 'refs/heads/V1_V2_merge'
with:
images: |
ghcr.io/stirling-tools/stirling-pdf-test
tags: |
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}
type=raw,value=latest
- name: Build and push Unified Dockerfile (latest variant)
id: build-push-latest
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
builder: ${{ steps.buildx.outputs.name }}
context: .
file: ./docker/embedded/Dockerfile
push: true
cache-from: type=gha
cache-to: type=gha,mode=max
tags: ${{ github.ref == 'refs/heads/V2-master' && steps.meta.outputs.tags || steps.meta-test.outputs.tags }}
labels: ${{ github.ref == 'refs/heads/V2-master' && steps.meta.outputs.labels || steps.meta-test.outputs.labels }}
build-args: VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }}
platforms: linux/amd64,linux/arm64/v8
provenance: true
sbom: true
- name: Sign regular images
if: github.ref == 'refs/heads/V2-master'
env:
DIGEST: ${{ steps.build-push-latest.outputs.digest }}
TAGS: ${{ steps.meta.outputs.tags }}
COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }}
COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
run: |
echo "$TAGS" | tr ',' '\n' | while read -r tag; do
cosign sign --yes \
--key env://COSIGN_PRIVATE_KEY \
"${tag}@${DIGEST}"
done
- name: Generate tags for latest-fat (V2-master branch - production)
id: meta-fat
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
if: github.ref == 'refs/heads/V2-master'
with:
images: |
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/s-pdf
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf
${{ secrets.DOCKER_HUB_ORG_USERNAME }}/stirling-pdf
tags: |
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}-fat
type=raw,value=latest-fat
- name: Generate tags for latest-fat (V1_V2_merge branch - test)
id: meta-fat-test
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
if: github.ref == 'refs/heads/V1_V2_merge'
with:
images: |
ghcr.io/stirling-tools/stirling-pdf-test
tags: |
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}-fat
type=raw,value=latest-fat
- name: Build and push Unified Dockerfile (fat variant)
id: build-push-fat
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
builder: ${{ steps.buildx.outputs.name }}
context: .
file: ./docker/embedded/Dockerfile.fat
push: true
cache-from: type=gha
cache-to: type=gha,mode=max
tags: ${{ github.ref == 'refs/heads/V2-master' && steps.meta-fat.outputs.tags || steps.meta-fat-test.outputs.tags }}
labels: ${{ github.ref == 'refs/heads/V2-master' && steps.meta-fat.outputs.labels || steps.meta-fat-test.outputs.labels }}
build-args: VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }}
platforms: linux/amd64,linux/arm64/v8
provenance: true
sbom: true
- name: Sign fat images
if: github.ref == 'refs/heads/V2-master'
env:
DIGEST: ${{ steps.build-push-fat.outputs.digest }}
TAGS: ${{ steps.meta-fat.outputs.tags }}
COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }}
COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
run: |
echo "$TAGS" | tr ',' '\n' | while read -r tag; do
cosign sign --key env://COSIGN_PRIVATE_KEY --yes "${tag}@${DIGEST}"
done
- name: Generate tags for ultra-lite (V2-master branch - production)
id: meta-lite
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
if: github.ref == 'refs/heads/V2-master'
with:
images: |
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/s-pdf
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf
${{ secrets.DOCKER_HUB_ORG_USERNAME }}/stirling-pdf
tags: |
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}-ultra-lite
type=raw,value=latest-ultra-lite
- name: Generate tags for ultra-lite (V1_V2_merge branch - test)
id: meta-lite-test
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
if: github.ref == 'refs/heads/V1_V2_merge'
with:
images: |
ghcr.io/stirling-tools/stirling-pdf-test
tags: |
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}-ultra-lite
type=raw,value=latest-ultra-lite
- name: Build and push Unified Dockerfile (ultra-lite variant)
id: build-push-lite
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
builder: ${{ steps.buildx.outputs.name }}
context: .
file: ./docker/embedded/Dockerfile.ultra-lite
push: true
cache-from: type=gha
cache-to: type=gha,mode=max
tags: ${{ github.ref == 'refs/heads/V2-master' && steps.meta-lite.outputs.tags || steps.meta-lite-test.outputs.tags }}
labels: ${{ github.ref == 'refs/heads/V2-master' && steps.meta-lite.outputs.labels || steps.meta-lite-test.outputs.labels }}
build-args: VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }}
platforms: linux/amd64,linux/arm64/v8
provenance: true
sbom: true
- name: Sign ultra-lite images
if: github.ref == 'refs/heads/V2-master'
env:
DIGEST: ${{ steps.build-push-lite.outputs.digest }}
TAGS: ${{ steps.meta-lite.outputs.tags }}
COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }}
COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
run: |
echo "$TAGS" | tr ',' '\n' | while read -r tag; do
cosign sign --key env://COSIGN_PRIVATE_KEY --yes "${tag}@${DIGEST}"
done
+11 -10
View File
@@ -24,17 +24,18 @@ permissions:
jobs:
push:
if: ${{ vars.CI_PROFILE != 'lite' }}
runs-on: ubuntu-latest
permissions:
packages: write
id-token: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
with:
egress-policy: audit
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Set up JDK 17
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
@@ -54,7 +55,7 @@ jobs:
- name: Install cosign
if: github.ref == 'refs/heads/master'
uses: sigstore/cosign-installer@d7543c93d881b35a8faa02e8e3605f69b7a1ce62 # v3.10.0
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
with:
cosign-release: "v2.4.1"
@@ -80,7 +81,7 @@ jobs:
password: ${{ github.token }}
- name: Set up QEMU
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
- name: Convert repository owner to lowercase
id: repoowner
@@ -88,7 +89,7 @@ jobs:
- name: Generate tags
id: meta
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
if: github.ref != 'refs/heads/main'
with:
images: |
@@ -107,7 +108,7 @@ jobs:
with:
builder: ${{ steps.buildx.outputs.name }}
context: .
file: ./Dockerfile
file: ./docker/embedded/Dockerfile
push: true
cache-from: type=gha
cache-to: type=gha,mode=max
@@ -134,7 +135,7 @@ jobs:
- name: Generate tags ultra-lite
id: meta2
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
if: github.ref != 'refs/heads/main'
with:
images: |
@@ -152,7 +153,7 @@ jobs:
if: github.ref != 'refs/heads/main'
with:
context: .
file: ./Dockerfile.ultra-lite
file: ./docker/embedded/Dockerfile.ultra-lite
push: true
cache-from: type=gha
cache-to: type=gha,mode=max
@@ -165,7 +166,7 @@ jobs:
- name: Generate tags fat
id: meta3
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
with:
images: |
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
@@ -183,7 +184,7 @@ jobs:
with:
builder: ${{ steps.buildx.outputs.name }}
context: .
file: ./Dockerfile.fat
file: ./docker/embedded/Dockerfile.fat
push: true
cache-from: type=gha
cache-to: type=gha,mode=max
+459 -104
View File
@@ -2,48 +2,31 @@ name: Release Artifacts
on:
workflow_dispatch:
release:
types: [created]
inputs:
platform:
description: "Platform to build (windows, macos, linux, or all)"
required: true
default: "all"
type: choice
options:
- all
- windows
- macos
- linux
push:
branches: [main, V2, V2-demo]
permissions:
contents: read
jobs:
build:
determine-matrix:
runs-on: ubuntu-latest
strategy:
matrix:
disable_security: [true, false]
include:
- disable_security: false
file_suffix: "-with-login"
- disable_security: true
file_suffix: ""
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
version: ${{ steps.versionNumber.outputs.versionNumber }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
with:
egress-policy: audit
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up JDK 17
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
with:
java-version: "17"
distribution: "temurin"
- uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
with:
gradle-version: 8.14
- name: Generate jar (Disable Security=${{ matrix.disable_security }})
run: ./gradlew clean createExe
env:
DISABLE_ADDITIONAL_FEATURES: ${{ matrix.disable_security }}
STIRLING_PDF_DESKTOP_UI: false
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Get version number
id: versionNumber
@@ -51,36 +34,413 @@ jobs:
VERSION=$(grep "^version =" build.gradle | awk -F'"' '{print $2}')
echo "versionNumber=$VERSION" >> $GITHUB_OUTPUT
- name: Rename binaries
- name: Determine build matrix
id: set-matrix
run: |
mv ./build/launch4j/Stirling-PDF.exe ./build/launch4j/Stirling-PDF-Server${{ matrix.file_suffix }}.exe
mv ./build/libs/Stirling-PDF-${{ steps.versionNumber.outputs.versionNumber }}.jar ./build/libs/Stirling-PDF${{ matrix.file_suffix }}.jar
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
case "${{ github.event.inputs.platform }}" in
"windows")
echo 'matrix={"include":[{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64"}]}' >> $GITHUB_OUTPUT
;;
"macos")
echo 'matrix={"include":[{"platform":"macos-15","args":"--target aarch64-apple-darwin","name":"macos-aarch64"},{"platform":"macos-15-intel","args":"--target x86_64-apple-darwin","name":"macos-x86_64"}]}' >> $GITHUB_OUTPUT
;;
"linux")
echo 'matrix={"include":[{"platform":"ubuntu-22.04","args":"","name":"linux-x86_64"}]}' >> $GITHUB_OUTPUT
;;
*)
echo 'matrix={"include":[{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64"},{"platform":"macos-15","args":"--target aarch64-apple-darwin","name":"macos-aarch64"},{"platform":"macos-15-intel","args":"--target x86_64-apple-darwin","name":"macos-x86_64"},{"platform":"ubuntu-22.04","args":"","name":"linux-x86_64"}]}' >> $GITHUB_OUTPUT
;;
esac
else
# For push events, build all platforms
echo 'matrix={"include":[{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64"},{"platform":"macos-15","args":"--target aarch64-apple-darwin","name":"macos-aarch64"},{"platform":"macos-15-intel","args":"--target x86_64-apple-darwin","name":"macos-x86_64"},{"platform":"ubuntu-22.04","args":"","name":"linux-x86_64"}]}' >> $GITHUB_OUTPUT
fi
- name: Debug build artifacts
build:
needs: determine-matrix
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.determine-matrix.outputs.matrix) }}
runs-on: ${{ matrix.platform }}
env:
SM_API_KEY: ${{ secrets.SM_API_KEY }}
WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
with:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Install dependencies (ubuntu only)
if: matrix.platform == 'ubuntu-22.04'
run: |
echo "Current Directory: $(pwd)"
ls -R ./build/libs
ls -R ./build/launch4j
sudo apt-get update
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libjavascriptcoregtk-4.0-dev libsoup2.4-dev libjavascriptcoregtk-4.1-dev libsoup-3.0-dev
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
cache-dependency-path: frontend/package-lock.json
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
with:
toolchain: stable
targets: ${{ (matrix.platform == 'macos-15' || matrix.platform == 'macos-15-intel') && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
- name: Set up JDK 21
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
with:
java-version: "21"
distribution: "temurin"
- name: Build Java backend with JLink
working-directory: ./
shell: bash
run: |
chmod +x ./gradlew
echo "🔧 Building Stirling-PDF JAR..."
./gradlew clean build -x spotlessApply -x spotlessCheck -x test -x sonarqube
# Find the built JAR
STIRLING_JAR=$(ls app/core/build/libs/stirling-pdf-*.jar | head -n 1)
echo "✅ Built JAR: $STIRLING_JAR"
# Create Tauri directories
mkdir -p ./frontend/src-tauri/libs
mkdir -p ./frontend/src-tauri/runtime
# Copy JAR to Tauri libs
cp "$STIRLING_JAR" ./frontend/src-tauri/libs/
echo "✅ JAR copied to Tauri libs"
# Analyze JAR dependencies for jlink modules
echo "🔍 Analyzing JAR dependencies..."
if command -v jdeps &> /dev/null; then
DETECTED_MODULES=$(jdeps --print-module-deps --ignore-missing-deps "$STIRLING_JAR" 2>/dev/null || echo "")
if [ -n "$DETECTED_MODULES" ]; then
echo "📋 jdeps detected modules: $DETECTED_MODULES"
MODULES="$DETECTED_MODULES,java.compiler,java.instrument,java.management,java.naming,java.net.http,java.prefs,java.rmi,java.scripting,java.security.jgss,java.security.sasl,java.sql,java.transaction.xa,java.xml.crypto,jdk.crypto.ec,jdk.crypto.cryptoki,jdk.unsupported"
else
echo "⚠️ jdeps analysis failed, using predefined modules"
MODULES="java.base,java.compiler,java.desktop,java.instrument,java.logging,java.management,java.naming,java.net.http,java.prefs,java.rmi,java.scripting,java.security.jgss,java.security.sasl,java.sql,java.transaction.xa,java.xml,java.xml.crypto,jdk.crypto.ec,jdk.crypto.cryptoki,jdk.unsupported"
fi
else
echo "⚠️ jdeps not available, using predefined modules"
MODULES="java.base,java.compiler,java.desktop,java.instrument,java.logging,java.management,java.naming,java.net.http,java.prefs,java.rmi,java.scripting,java.security.jgss,java.security.sasl,java.sql,java.transaction.xa,java.xml,java.xml.crypto,jdk.crypto.ec,jdk.crypto.cryptoki,jdk.unsupported"
fi
# Create custom JRE with jlink
echo "🔧 Creating custom JRE with jlink..."
echo "📋 Using modules: $MODULES"
# Remove any existing JRE
rm -rf ./frontend/src-tauri/runtime/jre
# Create the custom JRE
jlink \
--add-modules "$MODULES" \
--strip-debug \
--compress=2 \
--no-header-files \
--no-man-pages \
--output ./frontend/src-tauri/runtime/jre
if [ ! -d "./frontend/src-tauri/runtime/jre" ]; then
echo "❌ Failed to create JLink runtime"
exit 1
fi
# Test the bundled runtime
if [ -f "./frontend/src-tauri/runtime/jre/bin/java" ]; then
RUNTIME_VERSION=$(./frontend/src-tauri/runtime/jre/bin/java --version 2>&1 | head -n 1)
echo "✅ Custom JRE created successfully: $RUNTIME_VERSION"
else
echo "❌ Custom JRE executable not found"
exit 1
fi
# Calculate runtime size
RUNTIME_SIZE=$(du -sh ./frontend/src-tauri/runtime/jre | cut -f1)
echo "📊 Custom JRE size: $RUNTIME_SIZE"
env:
DISABLE_ADDITIONAL_FEATURES: true
- name: Install frontend dependencies
working-directory: ./frontend
run: npm install
# DigiCert KeyLocker Setup (Cloud HSM)
- name: Setup DigiCert KeyLocker
id: digicert-setup
if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && github.ref == 'refs/heads/main' }}
uses: digicert/ssm-code-signing@v1.1.0
env:
SM_API_KEY: ${{ secrets.SM_API_KEY }}
SM_CLIENT_CERT_FILE_B64: ${{ secrets.SM_CLIENT_CERT_FILE_B64 }}
SM_CLIENT_CERT_PASSWORD: ${{ secrets.SM_CLIENT_CERT_PASSWORD }}
SM_KEYPAIR_ALIAS: ${{ secrets.SM_KEYPAIR_ALIAS }}
SM_HOST: ${{ secrets.SM_HOST }}
- name: Setup DigiCert KeyLocker Certificate
if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && github.ref == 'refs/heads/main' }}
shell: pwsh
run: |
Write-Host "Setting up DigiCert KeyLocker environment..."
# Decode client certificate
$certBytes = [Convert]::FromBase64String("${{ secrets.SM_CLIENT_CERT_FILE_B64 }}")
$certPath = "D:\Certificate_pkcs12.p12"
[IO.File]::WriteAllBytes($certPath, $certBytes)
# Set environment variables
echo "SM_CLIENT_CERT_FILE=D:\Certificate_pkcs12.p12" >> $env:GITHUB_ENV
echo "SM_HOST=${{ secrets.SM_HOST }}" >> $env:GITHUB_ENV
echo "SM_API_KEY=${{ secrets.SM_API_KEY }}" >> $env:GITHUB_ENV
echo "SM_CLIENT_CERT_PASSWORD=${{ secrets.SM_CLIENT_CERT_PASSWORD }}" >> $env:GITHUB_ENV
echo "SM_KEYPAIR_ALIAS=${{ secrets.SM_KEYPAIR_ALIAS }}" >> $env:GITHUB_ENV
# Get PKCS11 config path from DigiCert action
$pkcs11Config = $env:PKCS11_CONFIG
if ($pkcs11Config) {
Write-Host "Found PKCS11_CONFIG: $pkcs11Config"
echo "PKCS11_CONFIG=$pkcs11Config" >> $env:GITHUB_ENV
} else {
Write-Host "PKCS11_CONFIG not set by DigiCert action, using default path"
$defaultPath = "C:\Users\RUNNER~1\AppData\Local\Temp\smtools-windows-x64\pkcs11properties.cfg"
if (Test-Path $defaultPath) {
Write-Host "Found config at default path: $defaultPath"
echo "PKCS11_CONFIG=$defaultPath" >> $env:GITHUB_ENV
} else {
Write-Host "Warning: Could not find PKCS11 config file"
}
}
# Traditional PFX Certificate Import (fallback if KeyLocker not configured)
- name: Import Windows Code Signing Certificate
if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY == '' && github.ref == 'refs/heads/main' }}
env:
WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }}
WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}
shell: powershell
run: |
if ($env:WINDOWS_CERTIFICATE) {
Write-Host "Importing Windows Code Signing Certificate..."
# Decode base64 certificate and save to file
$certBytes = [Convert]::FromBase64String($env:WINDOWS_CERTIFICATE)
$certPath = Join-Path $env:RUNNER_TEMP "certificate.pfx"
[IO.File]::WriteAllBytes($certPath, $certBytes)
# Import certificate to CurrentUser\My store
$cert = Import-PfxCertificate -FilePath $certPath -CertStoreLocation Cert:\CurrentUser\My -Password (ConvertTo-SecureString -String $env:WINDOWS_CERTIFICATE_PASSWORD -AsPlainText -Force)
# Extract and set thumbprint as environment variable
$thumbprint = $cert.Thumbprint
Write-Host "Certificate imported with thumbprint: $thumbprint"
echo "WINDOWS_CERTIFICATE_THUMBPRINT=$thumbprint" >> $env:GITHUB_ENV
# Clean up certificate file
Remove-Item $certPath
Write-Host "Windows certificate import completed."
} else {
Write-Host "⚠️ WINDOWS_CERTIFICATE secret not set - building unsigned binary"
}
- name: Import Apple Developer Certificate
if: matrix.platform == 'macos-15' || matrix.platform == 'macos-15-intel'
env:
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
run: |
echo "Importing Apple Developer Certificate..."
echo $APPLE_CERTIFICATE | base64 --decode > certificate.p12
# Create temporary keychain
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
KEYCHAIN_PASSWORD=$(openssl rand -base64 32)
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
# Import certificate
security import certificate.p12 -P "$APPLE_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
security list-keychain -d user -s $KEYCHAIN_PATH
security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
# Clean up
rm certificate.p12
- name: Verify Certificate
if: matrix.platform == 'macos-15' || matrix.platform == 'macos-15-intel'
run: |
echo "Verifying Apple Developer Certificate..."
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
CERT_INFO=$(security find-identity -v -p codesigning $KEYCHAIN_PATH | grep "Developer ID Application")
echo "Certificate Info: $CERT_INFO"
CERT_ID=$(echo "$CERT_INFO" | awk -F'"' '{print $2}')
echo "Certificate ID: $CERT_ID"
echo "APPLE_SIGNING_IDENTITY=$CERT_ID" >> $GITHUB_ENV
echo "Certificate imported successfully."
- name: Build Tauri app
uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ env.APPLE_SIGNING_IDENTITY }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPIMAGETOOL_SIGN_PASSPHRASE: ${{ secrets.APPIMAGETOOL_SIGN_PASSPHRASE }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY: ${{ secrets.VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY }}
VITE_SAAS_SERVER_URL: ${{ secrets.VITE_SAAS_SERVER_URL }}
# Only enable Windows signing in Tauri when on main
SIGN: ${{ github.ref == 'refs/heads/main' && (env.SM_API_KEY == '' && env.WINDOWS_CERTIFICATE != '') && '1' || '0' }}
CI: true
with:
projectPath: ./frontend
tauriScript: npx tauri
args: ${{ matrix.args }}
# Sign with DigiCert KeyLocker (post-build)
- name: Sign Windows binaries with DigiCert KeyLocker
if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && github.ref == 'refs/heads/main' }}
shell: pwsh
run: |
Write-Host "=== DigiCert KeyLocker Signing ==="
# Test smctl connectivity first
Write-Host "Testing smctl connection..."
$healthCheck = & smctl healthcheck 2>&1
if ($LASTEXITCODE -eq 0) {
Write-Host "[SUCCESS] Connected to DigiCert KeyLocker"
} else {
Write-Host "[ERROR] Failed to connect to DigiCert KeyLocker"
Write-Host $healthCheck
exit 1
}
Write-Host ""
# Sync certificates to Windows certificate store
Write-Host "Syncing certificates to Windows certificate store..."
$syncOutput = & smctl windows certsync 2>&1
Write-Host "Cert sync result: $syncOutput"
Write-Host ""
# Find only the files we need to sign (not build scripts)
$filesToSign = @()
# Main application executable
$mainExe = Get-ChildItem -Path "./frontend/src-tauri/target/x86_64-pc-windows-msvc/release" -Filter "stirling-pdf.exe" -File -ErrorAction SilentlyContinue
if ($mainExe) { $filesToSign += $mainExe }
# MSI installer
$msiFiles = Get-ChildItem -Path "./frontend/src-tauri/target" -Filter "*.msi" -Recurse -File
$filesToSign += $msiFiles
if ($filesToSign.Count -eq 0) {
Write-Host "[ERROR] No files found to sign"
exit 1
}
Write-Host "Found $($filesToSign.Count) files to sign:"
foreach ($f in $filesToSign) { Write-Host " - $($f.Name)" }
Write-Host ""
$signedCount = 0
foreach ($file in $filesToSign) {
Write-Host "Signing: $($file.Name)"
# Get PKCS11 config file path (set by DigiCert action)
$pkcs11Config = $env:PKCS11_CONFIG
if (-not $pkcs11Config) {
Write-Host "[ERROR] PKCS11_CONFIG environment variable not set"
Write-Host "DigiCert KeyLocker action may not have run correctly"
exit 1
}
Write-Host "Using PKCS11 config: $pkcs11Config"
# Try signing with certificate fingerprint first (if available)
$fingerprint = "${{ secrets.SM_CODE_SIGNING_CERT_SHA1_HASH }}"
if ($fingerprint -and $fingerprint -ne "") {
Write-Host "Attempting to sign with certificate fingerprint..."
$output = & smctl sign --fingerprint "$fingerprint" --input "$($file.FullName)" --config-file "$pkcs11Config" --verbose 2>&1
$exitCode = $LASTEXITCODE
} else {
Write-Host "No fingerprint provided, using keypair alias..."
# Use smctl to sign with keypair alias
$output = & smctl sign --keypair-alias "${{ secrets.SM_KEYPAIR_ALIAS }}" --input "$($file.FullName)" --config-file "$pkcs11Config" --verbose 2>&1
$exitCode = $LASTEXITCODE
}
Write-Host "Exit code: $exitCode"
Write-Host "Output: $output"
# Check if output contains "FAILED" even with exit code 0
if ($output -match "FAILED" -or $output -match "error" -or $output -match "Error") {
Write-Host ""
Write-Host "[ERROR] Signing failed for $($file.Name)"
Write-Host "[ERROR] smctl returned success but output indicates failure"
exit 1
}
if ($exitCode -ne 0) {
Write-Host "[ERROR] Failed to sign $($file.Name)"
Write-Host "Full error output:"
Write-Host $output
exit 1
}
$signedCount++
Write-Host "[SUCCESS] Signed: $($file.Name)"
Write-Host ""
}
Write-Host "=== Summary ==="
Write-Host "[SUCCESS] Signed $signedCount/$($filesToSign.Count) files successfully"
- name: Rename artifacts
shell: bash
run: |
mkdir -p ./dist
cd ./frontend/src-tauri/target
# Find and rename artifacts based on platform
if [ "${{ matrix.platform }}" = "windows-latest" ]; then
find . -name "*.exe" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.exe" \;
find . -name "*.msi" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.msi" \;
elif [ "${{ matrix.platform }}" = "macos-15" ] || [ "${{ matrix.platform }}" = "macos-15-intel" ]; then
find . -name "*.dmg" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.dmg" \;
find . -name "*.app" -exec cp -r {} "../../../dist/Stirling-PDF-${{ matrix.name }}.app" \;
else
find . -name "*.deb" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.deb" \;
find . -name "*.AppImage" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.AppImage" \;
fi
- name: Upload build artifacts
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: binaries${{ matrix.file_suffix }}
path: |
./build/launch4j/Stirling-PDF-Server${{ matrix.file_suffix }}.*
./build/libs/Stirling-PDF${{ matrix.file_suffix }}.*
name: Stirling-PDF-${{ matrix.name }}
path: ./dist/*
retention-days: 30
sign_verify:
needs: build
runs-on: ubuntu-latest
if: success()
strategy:
fail-fast: false
matrix:
disable_security: [true, false]
include:
- disable_security: false
file_suffix: "-with-login"
- disable_security: true
file_suffix: ""
name: [windows-x86_64, macos-aarch64, macos-x86_64, linux-x86_64]
steps:
- name: Harden Runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
@@ -88,9 +448,10 @@ jobs:
egress-policy: audit
- name: Download build artifacts
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with:
name: binaries${{ matrix.file_suffix }}
name: Stirling-PDF-${{ matrix.name }}
- name: Display structure of downloaded files
run: ls -R
@@ -101,80 +462,74 @@ jobs:
run: cosign generate-key-pair
- name: Sign and generate attestations
shell: bash
run: |
cosign sign-blob \
--key ./cosign.key \
--yes \
--output-signature ./libs/Stirling-PDF${{ matrix.file_suffix }}.jar.sig \
./libs/Stirling-PDF${{ matrix.file_suffix }}.jar
# Sign all artifacts for this platform
for file in *; do
if [ -f "$file" ] && [[ ! "$file" =~ \.(sig|intoto\.jsonl)$ ]]; then
echo "Signing: $file"
cosign attest-blob \
--predicate - \
--key ./cosign.key \
--yes \
--output-attestation ./libs/Stirling-PDF${{ matrix.file_suffix }}.jar.intoto.jsonl \
./libs/Stirling-PDF${{ matrix.file_suffix }}.jar
# Sign the artifact
cosign sign-blob \
--key ./cosign.key \
--yes \
--output-signature "${file}.sig" \
"$file"
cosign verify-blob \
--key ./cosign.pub \
--signature ./libs/Stirling-PDF${{ matrix.file_suffix }}.jar.sig \
./libs/Stirling-PDF${{ matrix.file_suffix }}.jar
# Generate attestation
cosign attest-blob \
--predicate - \
--key ./cosign.key \
--yes \
--output-attestation "${file}.intoto.jsonl" \
"$file"
cosign sign-blob \
--key ./cosign.key \
--yes \
--output-signature ./launch4j/Stirling-PDF-Server${{ matrix.file_suffix }}.exe.sig \
./launch4j/Stirling-PDF-Server${{ matrix.file_suffix }}.exe
# Verify the signature
cosign verify-blob \
--key ./cosign.pub \
--signature "${file}.sig" \
"$file"
cosign attest-blob \
--predicate - \
--key ./cosign.key \
--yes \
--output-attestation ./launch4j/Stirling-PDF-Server${{ matrix.file_suffix }}.exe.intoto.jsonl \
./launch4j/Stirling-PDF-Server${{ matrix.file_suffix }}.exe
cosign verify-blob \
--key ./cosign.pub \
--signature ./launch4j/Stirling-PDF-Server${{ matrix.file_suffix }}.exe.sig \
./launch4j/Stirling-PDF-Server${{ matrix.file_suffix }}.exe
echo "✅ Signed and verified: $file"
fi
done
- name: Upload signed artifacts
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: signed${{ matrix.file_suffix }}
name: Stirling-PDF-${{ matrix.name }}-signed
path: |
./libs/Stirling-PDF${{ matrix.file_suffix }}.*
./launch4j/Stirling-PDF-Server${{ matrix.file_suffix }}.*
*
!cosign.key
!cosign.pub
retention-days: 30
release:
needs: [build, sign_verify]
needs: [determine-matrix, build, sign_verify]
runs-on: ubuntu-latest
if: github.event_name == 'workflow_dispatch' || github.ref == 'refs/heads/main'
permissions:
contents: write
strategy:
matrix:
disable_security: [true, false]
include:
- disable_security: false
file_suffix: "-with-login"
- disable_security: true
file_suffix: ""
steps:
- name: Harden Runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
with:
egress-policy: audit
- name: Download signed artifacts
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
- name: Download all signed artifacts
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with:
name: signed${{ matrix.file_suffix }}
pattern: Stirling-PDF-*-signed
path: ./artifacts
- name: Upload binaries, attestations and signatures to Release and create GitHub Release
uses: softprops/action-gh-release@62c96d0c4e8a889135c1f3a25910db8dbe0e85f7 # v2.3.4
- name: Display structure of downloaded files
run: ls -R ./artifacts
- name: Create GitHub Release
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
with:
tag_name: v${{ needs.build.outputs.version }}
tag_name: v${{ needs.determine-matrix.outputs.version }}
generate_release_notes: true
files: |
./libs/Stirling-PDF*
./launch4j/Stirling-PDF-Server*
files: ./artifacts/**/*
draft: false
prerelease: false
+5 -4
View File
@@ -17,6 +17,7 @@ permissions: read-all
jobs:
analysis:
if: ${{ vars.CI_PROFILE != 'lite' }}
name: Scorecard analysis
runs-on: ubuntu-latest
permissions:
@@ -34,12 +35,12 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
with:
egress-policy: audit
- name: "Checkout code"
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
persist-credentials: false
@@ -66,7 +67,7 @@ jobs:
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
# format to the repository Actions tab.
- name: "Upload artifact"
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: SARIF file
path: results.sarif
@@ -74,6 +75,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@64d10c13136e1c5bce3e5fbde8d4906eeaafc885 # v3.29.5
uses: github/codeql-action/upload-sarif@fdbfb4d2750291e159f0156def62b853c2798ca2 # v3.29.5
with:
sarif_file: results.sarif
-75
View File
@@ -1,75 +0,0 @@
name: Run Sonarqube
on:
push:
branches:
- master
pull_request_target:
branches:
- main
workflow_dispatch:
# cancel in-progress jobs if a new job is triggered
# This is useful to avoid running multiple builds for the same branch if a new commit is pushed
# or a pull request is updated.
# It helps to save resources and time by ensuring that only the latest commit is built and tested
# This is particularly useful for long-running jobs that may take a while to complete.
# The `group` is set to a combination of the workflow name, event name, and branch name.
# This ensures that jobs are grouped by the workflow and branch, allowing for cancellation of
# in-progress jobs when a new commit is pushed to the same branch or a new pull request is opened.
concurrency:
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref_name || github.ref }}
cancel-in-progress: true
permissions:
pull-requests: read
actions: read
jobs:
sonarqube:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
with:
egress-policy: audit
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
fetch-depth: 0
- name: Setup Gradle
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
- name: Build and analyze with Gradle
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
DISABLE_ADDITIONAL_FEATURES: false
STIRLING_PDF_DESKTOP_UI: true
run: |
./gradlew clean build sonar \
-Dsonar.projectKey=Stirling-Tools_Stirling-PDF \
-Dsonar.organization=stirling-tools \
-Dsonar.host.url=https://sonarcloud.io \
-Dsonar.login=${SONAR_TOKEN} \
-Dsonar.log.level=DEBUG \
--info
- name: Upload Problems Report on Failure
if: failure()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: gradle-problems-report
path: build/reports/problems/problems-report.html
retention-days: 7
- name: Upload Sonar Logs on Failure
if: failure()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: sonar-logs
path: |
.scannerwork/report-task.txt
build/sonar/
retention-days: 7
+3 -2
View File
@@ -10,18 +10,19 @@ permissions:
jobs:
stale:
if: ${{ vars.CI_PROFILE != 'lite' }}
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
with:
egress-policy: audit
- name: 30 days stale issues
uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: 30
+3 -2
View File
@@ -23,14 +23,15 @@ permissions:
jobs:
push:
if: ${{ vars.CI_PROFILE != 'lite' }}
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
with:
egress-policy: audit
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Set up JDK 17
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
-122
View File
@@ -1,122 +0,0 @@
name: Sync Files
on:
workflow_dispatch:
push:
branches:
- main
paths:
- "build.gradle"
- "README.md"
- "app/core/src/main/resources/messages_*.properties"
- "app/core/src/main/resources/static/3rdPartyLicenses.json"
- "scripts/ignore_translation.toml"
# cancel in-progress jobs if a new job is triggered
# This is useful to avoid running multiple builds for the same branch if a new commit is pushed
# or a pull request is updated.
# It helps to save resources and time by ensuring that only the latest commit is built and tested
# This is particularly useful for long-running jobs that may take a while to complete.
# The `group` is set to a combination of the workflow name, event name, and branch name.
# This ensures that jobs are grouped by the workflow and branch, allowing for cancellation of
# in-progress jobs when a new commit is pushed to the same branch or a new pull request is opened.
concurrency:
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref_name || github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
sync-files:
runs-on: ubuntu-latest
env:
# Prevents sdist builds → no tar extraction
PIP_ONLY_BINARY: ":all:"
PIP_DISABLE_PIP_VERSION_CHECK: "1"
steps:
- name: Harden Runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
with:
egress-policy: audit
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup GitHub App Bot
id: setup-bot
uses: ./.github/actions/setup-bot
with:
app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- name: Set up Python
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with:
python-version: "3.12"
cache: "pip" # caching pip dependencies
- name: Sync translation property files
run: |
python .github/scripts/check_language_properties.py --reference-file "app/core/src/main/resources/messages_en_GB.properties" --branch main
- name: Commit translation files
run: |
git add app/core/src/main/resources/messages_*.properties
git diff --staged --quiet || git commit -m ":memo: Sync translation files" || echo "No changes detected"
- name: Install dependencies
# Wheels-only + Hash-Pinning
run: |
pip install --require-hashes --only-binary=:all: -r ./.github/scripts/requirements_sync_readme.txt
- name: Sync README.md
run: |
python scripts/counter_translation.py
- name: Run git add
run: |
git add README.md scripts/ignore_translation.toml
git diff --staged --quiet || git commit -m ":memo: Sync README.md & scripts/ignore_translation.toml" || echo "No changes detected"
- name: Create Pull Request
if: always()
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
with:
token: ${{ steps.setup-bot.outputs.token }}
commit-message: Update files
committer: ${{ steps.setup-bot.outputs.committer }}
author: ${{ steps.setup-bot.outputs.committer }}
signoff: true
branch: sync_readme
title: ":globe_with_meridians: Sync Translations + Update README Progress Table"
body: |
### Description of Changes
This Pull Request was automatically generated to synchronize updates to translation files and documentation. Below are the details of the changes made:
#### **1. Synchronization of Translation Files**
- Updated translation files (`messages_*.properties`) to reflect changes in the reference file `messages_en_GB.properties`.
- Ensured consistency and synchronization across all supported language files.
- Highlighted any missing or incomplete translations.
#### **2. Update README.md**
- Generated the translation progress table in `README.md`.
- Added a summary of the current translation status for all supported languages.
- Included up-to-date statistics on translation coverage.
#### **Why these changes are necessary**
- Keeps translation files aligned with the latest reference updates.
- Ensures the documentation reflects the current translation progress.
---
Auto-generated by [create-pull-request][1].
[1]: https://github.com/peter-evans/create-pull-request
draft: false
delete-branch: true
labels: github-actions
sign-commits: true
add-paths: |
README.md
app/core/src/main/resources/messages_*.properties
+26 -20
View File
@@ -1,15 +1,15 @@
name: Sync Files V2
name: Sync Files (TOML)
on:
workflow_dispatch:
push:
branches:
- V2
- main
- syncLangTest
paths:
- "build.gradle"
- "README.md"
- "frontend/public/locales/*/translation.json"
- "frontend/public/locales/*/translation.toml"
- "app/core/src/main/resources/static/3rdPartyLicenses.json"
- "scripts/ignore_translation.toml"
@@ -33,11 +33,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
with:
egress-policy: audit
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Setup GitHub App Bot
id: setup-bot
@@ -47,26 +47,30 @@ jobs:
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- name: Set up Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with:
python-version: "3.12"
cache: "pip" # caching pip dependencies
- name: Sync translation JSON files
- name: Install Python dependencies
run: |
python .github/scripts/check_language_json.py --reference-file "frontend/public/locales/en-GB/translation.json" --branch V2
pip install tomli-w
- name: Sync translation TOML files
run: |
python .github/scripts/check_language_toml.py --reference-file "frontend/public/locales/en-GB/translation.toml" --branch main
- name: Commit translation files
run: |
git add frontend/public/locales/*/translation.json
git diff --staged --quiet || git commit -m ":memo: Sync translation files" || echo "No changes detected"
git add frontend/public/locales/*/translation.toml
git diff --staged --quiet || git commit -m ":memo: Sync translation files (TOML)" || echo "No changes detected"
- name: Install dependencies
- name: Install README dependencies
run: pip install --require-hashes -r ./.github/scripts/requirements_sync_readme.txt
- name: Sync README.md
run: |
python scripts/counter_translation_v2.py
python scripts/counter_translation_v3.py
- name: Run git add
run: |
@@ -75,28 +79,29 @@ jobs:
- name: Create Pull Request
if: always()
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
with:
token: ${{ steps.setup-bot.outputs.token }}
commit-message: Update files
committer: ${{ steps.setup-bot.outputs.committer }}
author: ${{ steps.setup-bot.outputs.committer }}
signoff: true
branch: sync_readme_v2
base: V2
title: ":globe_with_meridians: [V2] Sync Translations + Update README Progress Table"
branch: sync_readme_v3
base: main
title: ":globe_with_meridians: Sync Translations + Update README Progress Table"
body: |
### Description of Changes
This Pull Request was automatically generated to synchronize updates to translation files and documentation for the **V2 branch**. Below are the details of the changes made:
This Pull Request was automatically generated to synchronize updates to translation files and documentation. Below are the details of the changes made:
#### **1. Synchronization of Translation Files**
- Updated translation files (`frontend/public/locales/*/translation.json`) to reflect changes in the reference file `en-GB/translation.json`.
- Updated translation files (`frontend/public/locales/*/translation.toml`) to reflect changes in the reference file `en-GB/translation.toml`.
- Ensured consistency and synchronization across all supported language files.
- Highlighted any missing or incomplete translations.
- **Format**: TOML
#### **2. Update README.md**
- Generated the translation progress table in `README.md`.
- Generated the translation progress table in `README.md` using `counter_translation_v3.py`.
- Added a summary of the current translation status for all supported languages.
- Included up-to-date statistics on translation coverage.
@@ -115,4 +120,5 @@ jobs:
sign-commits: true
add-paths: |
README.md
frontend/public/locales/*/translation.json
frontend/public/locales/*/translation.toml
scripts/ignore_translation.toml
+310 -4
View File
@@ -14,7 +14,7 @@ on:
- macos
- linux
pull_request:
branches: [main, V2]
branches: [main, V2, V2-tauri-windows]
paths:
- 'frontend/src-tauri/**'
- 'frontend/src/desktop/**'
@@ -28,6 +28,7 @@ permissions:
jobs:
determine-matrix:
if: ${{ vars.CI_PROFILE != 'lite' }}
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
@@ -61,6 +62,9 @@ jobs:
fail-fast: false
matrix: ${{ fromJson(needs.determine-matrix.outputs.matrix) }}
runs-on: ${{ matrix.platform }}
env:
SM_API_KEY: ${{ secrets.SM_API_KEY }}
WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
@@ -89,8 +93,6 @@ jobs:
toolchain: stable
targets: ${{ (matrix.platform == 'macos-15' || matrix.platform == 'macos-15-intel') && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
- name: Set up JDK 21
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
with:
@@ -174,6 +176,84 @@ jobs:
working-directory: ./frontend
run: npm install
# DigiCert KeyLocker Setup (Cloud HSM)
- name: Setup DigiCert KeyLocker
id: digicert-setup
if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && github.ref == 'refs/heads/main' }}
uses: digicert/ssm-code-signing@v1.1.0
env:
SM_API_KEY: ${{ secrets.SM_API_KEY }}
SM_CLIENT_CERT_FILE_B64: ${{ secrets.SM_CLIENT_CERT_FILE_B64 }}
SM_CLIENT_CERT_PASSWORD: ${{ secrets.SM_CLIENT_CERT_PASSWORD }}
SM_KEYPAIR_ALIAS: ${{ secrets.SM_KEYPAIR_ALIAS }}
SM_HOST: ${{ secrets.SM_HOST }}
- name: Setup DigiCert KeyLocker Certificate
if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && github.ref == 'refs/heads/main' }}
shell: pwsh
run: |
Write-Host "Setting up DigiCert KeyLocker environment..."
# Decode client certificate
$certBytes = [Convert]::FromBase64String("${{ secrets.SM_CLIENT_CERT_FILE_B64 }}")
$certPath = "D:\Certificate_pkcs12.p12"
[IO.File]::WriteAllBytes($certPath, $certBytes)
# Set environment variables
echo "SM_CLIENT_CERT_FILE=D:\Certificate_pkcs12.p12" >> $env:GITHUB_ENV
echo "SM_HOST=${{ secrets.SM_HOST }}" >> $env:GITHUB_ENV
echo "SM_API_KEY=${{ secrets.SM_API_KEY }}" >> $env:GITHUB_ENV
echo "SM_CLIENT_CERT_PASSWORD=${{ secrets.SM_CLIENT_CERT_PASSWORD }}" >> $env:GITHUB_ENV
echo "SM_KEYPAIR_ALIAS=${{ secrets.SM_KEYPAIR_ALIAS }}" >> $env:GITHUB_ENV
# Get PKCS11 config path from DigiCert action
$pkcs11Config = $env:PKCS11_CONFIG
if ($pkcs11Config) {
Write-Host "Found PKCS11_CONFIG: $pkcs11Config"
echo "PKCS11_CONFIG=$pkcs11Config" >> $env:GITHUB_ENV
} else {
Write-Host "PKCS11_CONFIG not set by DigiCert action, using default path"
$defaultPath = "C:\Users\RUNNER~1\AppData\Local\Temp\smtools-windows-x64\pkcs11properties.cfg"
if (Test-Path $defaultPath) {
Write-Host "Found config at default path: $defaultPath"
echo "PKCS11_CONFIG=$defaultPath" >> $env:GITHUB_ENV
} else {
Write-Host "Warning: Could not find PKCS11 config file"
}
}
# Traditional PFX Certificate Import (fallback if KeyLocker not configured)
- name: Import Windows Code Signing Certificate
if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY == '' && github.ref == 'refs/heads/main' }}
env:
WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }}
WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}
shell: powershell
run: |
if ($env:WINDOWS_CERTIFICATE) {
Write-Host "Importing Windows Code Signing Certificate..."
# Decode base64 certificate and save to file
$certBytes = [Convert]::FromBase64String($env:WINDOWS_CERTIFICATE)
$certPath = Join-Path $env:RUNNER_TEMP "certificate.pfx"
[IO.File]::WriteAllBytes($certPath, $certBytes)
# Import certificate to CurrentUser\My store
$cert = Import-PfxCertificate -FilePath $certPath -CertStoreLocation Cert:\CurrentUser\My -Password (ConvertTo-SecureString -String $env:WINDOWS_CERTIFICATE_PASSWORD -AsPlainText -Force)
# Extract and set thumbprint as environment variable
$thumbprint = $cert.Thumbprint
Write-Host "Certificate imported with thumbprint: $thumbprint"
echo "WINDOWS_CERTIFICATE_THUMBPRINT=$thumbprint" >> $env:GITHUB_ENV
# Clean up certificate file
Remove-Item $certPath
Write-Host "Windows certificate import completed."
} else {
Write-Host "⚠️ WINDOWS_CERTIFICATE secret not set - building unsigned binary"
}
- name: Import Apple Developer Certificate
if: matrix.platform == 'macos-15' || matrix.platform == 'macos-15-intel'
env:
@@ -229,13 +309,177 @@ jobs:
APPLE_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPIMAGETOOL_SIGN_PASSPHRASE: ${{ secrets.APPIMAGETOOL_SIGN_PASSPHRASE }}
SIGN: 1
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY: ${{ secrets.VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY }}
VITE_SAAS_SERVER_URL: ${{ secrets.VITE_SAAS_SERVER_URL }}
# Only enable Windows signing in Tauri when on main
SIGN: ${{ github.ref == 'refs/heads/main' && (env.SM_API_KEY == '' && env.WINDOWS_CERTIFICATE != '') && '1' || '0' }}
CI: true
with:
projectPath: ./frontend
tauriScript: npx tauri
args: ${{ matrix.args }}
# Sign with DigiCert KeyLocker (post-build)
- name: Sign Windows binaries with DigiCert KeyLocker
if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && github.ref == 'refs/heads/main' }}
shell: pwsh
run: |
Write-Host "=== DigiCert KeyLocker Signing ==="
# Test smctl connectivity first
Write-Host "Testing smctl connection..."
$healthCheck = & smctl healthcheck 2>&1
if ($LASTEXITCODE -eq 0) {
Write-Host "[SUCCESS] Connected to DigiCert KeyLocker"
} else {
Write-Host "[ERROR] Failed to connect to DigiCert KeyLocker"
Write-Host $healthCheck
exit 1
}
Write-Host ""
# Sync certificates to Windows certificate store
Write-Host "Syncing certificates to Windows certificate store..."
$syncOutput = & smctl windows certsync 2>&1
Write-Host "Cert sync result: $syncOutput"
Write-Host ""
# List available certificates and check if they have certificates attached
Write-Host "Checking for available certificates..."
$certList = & smctl keypair ls 2>&1
Write-Host "Keypair list output:"
Write-Host $certList
Write-Host ""
# Parse the output to check certificate status
$lines = $certList -split "`n"
$foundKeypair = $false
$hasCertificate = $false
foreach ($line in $lines) {
if ($line -match "${{ secrets.SM_KEYPAIR_ALIAS }}") {
$foundKeypair = $true
Write-Host "[SUCCESS] Found keypair in list"
# Check if this line has certificate info (not just empty spaces after alias)
$parts = $line -split "\s+"
if ($parts.Count -gt 2 -and $parts[1] -ne "" -and $parts[1] -ne "CERTIFICATE") {
$hasCertificate = $true
Write-Host "[SUCCESS] Certificate is associated with keypair"
}
}
}
if (-not $foundKeypair) {
Write-Host "[ERROR] Keypair not found: ${{ secrets.SM_KEYPAIR_ALIAS }}"
Write-Host "Available keypairs are listed above"
Write-Host ""
Write-Host "Please verify:"
Write-Host " 1. Keypair alias is correct in GitHub secret"
Write-Host " 2. API key has access to this keypair"
exit 1
}
if (-not $hasCertificate) {
Write-Host "[ERROR] No certificate associated with keypair"
Write-Host "This usually means:"
Write-Host " 1. Certificate not yet synced to KeyLocker (run sync manually)"
Write-Host " 2. Certificate is pending approval"
Write-Host " 3. Certificate needs to be attached to the keypair"
Write-Host ""
Write-Host "Try running in DigiCert ONE portal:"
Write-Host " smctl keypair sync"
exit 1
}
Write-Host "[SUCCESS] Certificate check passed"
Write-Host ""
# Find only the files we need to sign (not build scripts)
$filesToSign = @()
# Main application executable
$mainExe = Get-ChildItem -Path "./frontend/src-tauri/target/x86_64-pc-windows-msvc/release" -Filter "stirling-pdf.exe" -File -ErrorAction SilentlyContinue
if ($mainExe) { $filesToSign += $mainExe }
# MSI installer
$msiFiles = Get-ChildItem -Path "./frontend/src-tauri/target" -Filter "*.msi" -Recurse -File
$filesToSign += $msiFiles
if ($filesToSign.Count -eq 0) {
Write-Host "[ERROR] No files found to sign"
exit 1
}
Write-Host "Found $($filesToSign.Count) files to sign:"
foreach ($f in $filesToSign) { Write-Host " - $($f.Name)" }
Write-Host ""
$signedCount = 0
foreach ($file in $filesToSign) {
Write-Host "Signing: $($file.Name)"
# Get PKCS11 config file path (set by DigiCert action)
$pkcs11Config = $env:PKCS11_CONFIG
if (-not $pkcs11Config) {
Write-Host "[ERROR] PKCS11_CONFIG environment variable not set"
Write-Host "DigiCert KeyLocker action may not have run correctly"
exit 1
}
Write-Host "Using PKCS11 config: $pkcs11Config"
# Try signing with certificate fingerprint first (if available)
$fingerprint = "${{ secrets.SM_CODE_SIGNING_CERT_SHA1_HASH }}"
if ($fingerprint -and $fingerprint -ne "") {
Write-Host "Attempting to sign with certificate fingerprint..."
$output = & smctl sign --fingerprint "$fingerprint" --input "$($file.FullName)" --config-file "$pkcs11Config" --verbose 2>&1
$exitCode = $LASTEXITCODE
} else {
Write-Host "No fingerprint provided, using keypair alias..."
# Use smctl to sign with keypair alias
$output = & smctl sign --keypair-alias "${{ secrets.SM_KEYPAIR_ALIAS }}" --input "$($file.FullName)" --config-file "$pkcs11Config" --verbose 2>&1
$exitCode = $LASTEXITCODE
}
Write-Host "Exit code: $exitCode"
Write-Host "Output: $output"
# Check if output contains "FAILED" even with exit code 0
if ($output -match "FAILED" -or $output -match "error" -or $output -match "Error") {
Write-Host ""
Write-Host "[ERROR] Signing failed for $($file.Name)"
Write-Host "[ERROR] smctl returned success but output indicates failure"
Write-Host ""
Write-Host "Possible issues:"
Write-Host " 1. Certificate not fully synced to KeyLocker (wait a few minutes)"
Write-Host " 2. Incorrect keypair alias"
Write-Host " 3. API key lacks signing permissions"
Write-Host ""
Write-Host "Please verify in DigiCert ONE portal:"
Write-Host " - Certificate status is 'Issued' (not Pending)"
Write-Host " - Keypair status is 'Online'"
Write-Host " - 'Can sign' is set to 'Yes'"
exit 1
}
if ($exitCode -ne 0) {
Write-Host "[ERROR] Failed to sign $($file.Name)"
Write-Host "Full error output:"
Write-Host $output
exit 1
}
$signedCount++
Write-Host "[SUCCESS] Signed: $($file.Name)"
Write-Host ""
}
Write-Host "=== Summary ==="
Write-Host "[SUCCESS] Signed $signedCount/$($filesToSign.Count) files successfully"
- name: Verify notarization (macOS only)
if: matrix.platform == 'macos-15' || matrix.platform == 'macos-15-intel'
run: |
@@ -269,6 +513,66 @@ jobs:
find . -name "*.AppImage" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.AppImage" \;
fi
- name: Verify Windows Code Signature
if: matrix.platform == 'windows-latest' && github.ref == 'refs/heads/main'
shell: pwsh
run: |
Write-Host "Verifying Windows code signatures..."
$exePath = "./dist/Stirling-PDF-${{ matrix.name }}.exe"
$msiPath = "./dist/Stirling-PDF-${{ matrix.name }}.msi"
$allSigned = $true
$usingKeyLocker = "${{ env.SM_API_KEY }}" -ne ""
$usingPfx = "${{ env.WINDOWS_CERTIFICATE }}" -ne ""
# Check EXE signature
if (Test-Path $exePath) {
$exeSig = Get-AuthenticodeSignature -FilePath $exePath
Write-Host "EXE Signature Status: $($exeSig.Status)"
Write-Host "EXE Signer: $($exeSig.SignerCertificate.Subject)"
Write-Host "EXE Timestamp: $($exeSig.TimeStamperCertificate.NotAfter)"
if ($exeSig.Status -ne "Valid") {
Write-Host "[WARNING] EXE is not properly signed (Status: $($exeSig.Status))"
if ($usingKeyLocker -or $usingPfx) {
Write-Host "[ERROR] Certificate was provided but signing failed"
$allSigned = $false
} else {
Write-Host "[INFO] Building unsigned binary (no certificate provided)"
}
} else {
Write-Host "[SUCCESS] EXE is properly signed"
}
}
# Check MSI signature
if (Test-Path $msiPath) {
$msiSig = Get-AuthenticodeSignature -FilePath $msiPath
Write-Host "MSI Signature Status: $($msiSig.Status)"
Write-Host "MSI Signer: $($msiSig.SignerCertificate.Subject)"
Write-Host "MSI Timestamp: $($msiSig.TimeStamperCertificate.NotAfter)"
if ($msiSig.Status -ne "Valid") {
Write-Host "[WARNING] MSI is not properly signed (Status: $($msiSig.Status))"
if ($usingKeyLocker -or $usingPfx) {
Write-Host "[ERROR] Certificate was provided but signing failed"
$allSigned = $false
} else {
Write-Host "[INFO] Building unsigned binary (no certificate provided)"
}
} else {
Write-Host "[SUCCESS] MSI is properly signed"
}
}
if (($usingKeyLocker -or $usingPfx) -and -not $allSigned) {
Write-Host "[ERROR] Code signing verification failed"
exit 1
} else {
Write-Host "[SUCCESS] Code signature verification completed"
}
- name: Upload artifacts
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
@@ -333,6 +637,8 @@ jobs:
if [ "${{ needs.build.result }}" = "success" ]; then
echo "✅ All Tauri builds completed successfully!"
echo "Artifacts are ready for distribution."
elif [ "${{ needs.build.result }}" = "skipped" ]; then
echo "⏭️ Tauri builds skipped (CI lite mode enabled)"
else
echo "❌ Some Tauri builds failed."
echo "Please check the logs and fix any issues."
+9 -8
View File
@@ -21,15 +21,16 @@ permissions:
jobs:
deploy:
if: ${{ vars.CI_PROFILE != 'lite' }}
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
with:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Set up JDK
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
@@ -66,7 +67,7 @@ jobs:
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
context: .
file: ./Dockerfile
file: ./docker/embedded/Dockerfile
push: true
tags: ${{ secrets.DOCKER_HUB_USERNAME }}/test:test-${{ github.sha }}
build-args: VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }}
@@ -124,7 +125,7 @@ jobs:
outputs:
frontend: ${{ steps.changes.outputs.frontend }}
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Check for file changes
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
@@ -139,14 +140,14 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
with:
egress-policy: audit
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Set up Node
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with:
cache: 'npm'
cache-dependency-path: frontend/package-lock.json
@@ -175,7 +176,7 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
with:
egress-policy: audit
+35 -2
View File
@@ -17,8 +17,8 @@ local.properties
version.properties
#### Stirling-PDF Files ###
pipeline/watchedFolders/
pipeline/finishedFolders/
pipeline/
!pipeline/.gitkeep
customFiles/
configs/
watchedFolders/
@@ -31,8 +31,28 @@ exampleYmlFiles/stirling/
/testing/file_snapshots
SwaggerDoc.json
# Docker bind-mount volumes (local data)
AI-Document-Generator-main/backend/data/
AI-Document-Generator-main/backend/output/
docker/compose/stirling/
# Frontend build artifacts copied to backend static resources
# These are generated by npm build and should not be committed
app/core/src/main/resources/static/assets/
app/core/src/main/resources/static/index.html
app/core/src/main/resources/static/locales/
app/core/src/main/resources/static/Login/
app/core/src/main/resources/static/classic-logo/
app/core/src/main/resources/static/modern-logo/
app/core/src/main/resources/static/og_images/
app/core/src/main/resources/static/samples/
app/core/src/main/resources/static/manifest-classic.json
app/core/src/main/resources/static/robots.txt
# Note: Keep backend-managed files like fonts/, css/, js/, pdfjs/, etc.
# Gradle
.gradle
.gradle-home
.lock
# External tool builders
@@ -213,3 +233,16 @@ node_modules/
test_batch.json
*.backup.*.json
frontend/public/locales/*/translation.backup*.json
# Development/build artifacts
.gradle-cache/
scripts/pdf-collection/
**/tmp/
*.backup
# Type3 development data
docs/type3/signatures/
# Type3 sample PDFs (development only)
**/type3/samples/
+2 -2
View File
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.12.7
rev: v0.14.2
hooks:
- id: ruff
args:
@@ -26,7 +26,7 @@ repos:
hooks:
- id: gitleaks
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
rev: v6.0.0
hooks:
- id: end-of-file-fixer
files: ^.*(\.js|\.java|\.py|\.yml)$
+3 -3
View File
@@ -202,10 +202,10 @@ const [ToolName] = (props: BaseToolProps) => {
## 5. Add Translations
Update translation files. **Important: Only update `en-GB` files** - other languages are handled separately.
**File to update:** `frontend/public/locales/en-GB/translation.json`
**File to update:** `frontend/public/locales/en-GB/translation.toml`
**Required Translation Keys**:
```json
```toml
{
"home": {
"[toolName]": {
@@ -251,7 +251,7 @@ Update translation files. **Important: Only update `en-GB` files** - other langu
```
**Translation Notes:**
- **Only update `en-GB/translation.json`** - other locale files are managed separately
- **Only update `en-GB/translation.toml`** - other locale files are managed separately
- Use descriptive keys that match your component's `t()` calls
- Include tooltip translations if you created tooltip hooks
- Add `options.*` keys if your tool has settings with descriptions
+45
View File
@@ -0,0 +1,45 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
env/
venv/
ENV/
build/
dist/
*.egg-info/
# Node
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
dist/
.vite/
# Environment
.env
.env.local
# LaTeX outputs
*.aux
*.log
*.out
*.toc
*.pdf
*.tex
backend/output/
backend/data/user_styles.json
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
+32
View File
@@ -0,0 +1,32 @@
# syntax=docker/dockerfile:1.5
FROM python:3.11-slim
# Install full TeXLive so LLM outputs (siunitx, paracol, tikz, etc.) compile reliably.
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
apt-get update && \
apt-get install -y --no-install-recommends \
texlive-full \
latexmk \
ghostscript \
poppler-utils \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
# Set working directory
WORKDIR /app
# Copy backend files
COPY backend/requirements.txt .
RUN --mount=type=cache,target=/root/.cache/pip \
pip install -r requirements.txt
COPY backend/ .
# Create output directories
RUN mkdir -p /app/output /app/data
# Expose port
EXPOSE 5000
# Run the Flask app
CMD ["python", "app.py"]
+173
View File
@@ -0,0 +1,173 @@
# LaTeX PDF Generator
AI-powered document generator using LaTeX. Creates professional documents (invoices, resumes, contracts, etc.) from natural language prompts with conversational editing.
## Features
- 🎨 ChatGPT-like interface with split-screen PDF preview (main window only)
- 📝 Generates LaTeX documents from natural language and compiles to PDF in Docker
- 🔄 Conversational editing with PDF regeneration on every turn
- 💾 Style memory + template reuse per user/team and document type
- 🗂️ Version history with per-iteration PDFs you can reopen
- 📄 Supports: Invoices, Resumes, Contracts, Letters, Reports, poems, proposals, and more
## Quick Start
### Option 1: Docker (Recommended)
```bash
# Build the Docker image
docker build -t latex-generator .
# Run the backend
docker run -p 5000:5000 latex-generator
```
### Option 2: Local Development
#### Backend Setup
```bash
cd backend
# Install Python dependencies
pip install -r requirements.txt
# Install LaTeX (if not already installed)
# On Ubuntu/Debian:
sudo apt-get install texlive-latex-base texlive-latex-extra
# On Mac:
brew install --cask mactex
# On Windows:
# Download and install MiKTeX from https://miktex.org/download
# Run the backend
python app.py
```
Backend will run on `http://localhost:5000`
#### Frontend Setup
```bash
cd frontend
# Install dependencies
npm install
# Run the dev server
npm run dev
```
Frontend will run on `http://localhost:3000`
## Usage
1. Open `http://localhost:3000` in your browser
2. Type a prompt like "Create an invoice for web development services"
3. The AI generates LaTeX code and compiles it to PDF
4. Continue chatting to refine the document
5. Download the final PDF
## Example Prompts
- "Create a professional invoice for $1,500 in consulting services"
- "Generate a resume for a senior software engineer with 5 years experience"
- "Make a business letter to a client about project completion"
- "Create a contract for freelance web development"
## Configuration
### OpenAI API (Optional)
To use real AI generation instead of mock templates:
1. Copy `.env.example` to `.env`
2. Add your OpenAI API key: `OPENAI_API_KEY=sk-...`
3. (Optional) Choose models:
- Smart (full generation): `SMART_MODEL=gpt-5.1` (default)
- Fast (intent/pre checks): `FAST_MODEL=gpt-4.1-nano` (default)
3. (Legacy SDK) We pin `openai==0.28.1` in Docker to avoid client init issues. No code changes needed.
## Project Structure
```
latex-pdf-generator/
├── frontend/ # React + TypeScript + Tailwind
│ ├── src/
│ │ ├── components/
│ │ │ ├── landing/ # Landing hero + CTA
│ │ │ ├── modals/ # Reusable modal(s)
│ │ │ ├── workspace/ # Chat panel, preview, history
│ │ │ └── ui/ # Buttons, button groups, etc.
│ │ ├── hooks/ # Workflow + speech capture hooks
│ │ ├── types/ # Shared TypeScript interfaces
│ │ ├── App.tsx # Thin orchestrator
│ │ └── main.tsx
│ ├── .eslintrc.cjs # ESLint config (React + TS)
│ ├── package.json
│ └── vite.config.ts
├── backend/ # Flask + LaTeX
│ ├── app.py # Routes + Flask app factory
│ ├── ai_generation.py # OpenAI + mock generation helpers
│ ├── briefs.py # Guided brief collection utilities
│ ├── config.py # Logging + environment setup
│ ├── document_types.py # Doc-type heuristics
│ ├── latex_utils.py # LaTeX sanitizers + layout helpers
│ ├── pdf_utils.py # PDF compilation + render helpers
│ ├── storage.py # JSON persistence for users/templates
│ ├── styles.py # Style preference heuristics
│ ├── vision.py # Layout extraction via multimodal GPT
│ ├── requirements.txt
│ └── data/ # User style storage
├── Dockerfile
└── README.md
```
### Linting
The frontend now ships with ESLint + TypeScript rules that keep the new modular structure tidy:
```bash
cd frontend
npm run lint
```
Backend linting can be added with your preferred tool (e.g., ruff or flake8) by pointing it at the new small modules in `backend/`.
## Troubleshooting
### LaTeX compilation fails
- Ensure `pdflatex` is in your PATH
- Check logs in the backend console
- Verify LaTeX packages are installed
### CORS errors
- Make sure both frontend and backend are running
- Frontend proxy is configured in `vite.config.ts`
### PDF not displaying
- Check browser console for errors
- Ensure the backend `/output` endpoint is accessible
- Try opening the PDF URL directly
## Development
### Mock Mode (Current)
The app currently uses mock LaTeX templates for quick testing. To enable real AI:
1. Get an OpenAI API key
2. Set `OPENAI_API_KEY` and optionally override:
- `SMART_MODEL` (default `gpt-5.1`)
- `FAST_MODEL` (default `gpt-4.1-nano`)
3. Restart the backend; it auto-detects whether to call the live model or the bundled mock templates
## License
MIT
@@ -0,0 +1,659 @@
from __future__ import annotations
import json
import re
from typing import Any, Dict, List, Optional
import time
from config import CLIENT_MODE, SMART_MODEL, STREAMING_ENABLED, get_chat_model, logger
from langchain_utils import to_lc_messages
from storage import save_user_style
from prompts import latex_system_prompt, latex_context_messages
def generate_outline_with_llm(
prompt: str,
document_type: str,
constraints: Optional[Dict[str, Any]] = None,
) -> str:
if CLIENT_MODE == "langchain":
constraint_text = ""
if constraints:
tone = constraints.get("tone")
audience = constraints.get("audience")
pages = constraints.get("pageCount")
constraint_text = f"Tone: {tone}. Audience: {audience}. Target pages: {pages}."
system_prompt = (
"You are an outline generator for document creation.\n"
f"Document type: {document_type}\n"
f"{constraint_text}\n"
"Return a concise outline with section titles and short descriptions.\n"
"Keep each description to roughly 6-12 words.\n"
"Ensure the outline scope fits the target page count.\n"
"Output plain text only, using a numbered list with 5-9 sections."
)
messages: List[Dict[str, Any]] = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": prompt},
]
try:
llm = get_chat_model(SMART_MODEL)
if llm:
start = time.perf_counter()
response = llm.invoke(to_lc_messages(messages))
elapsed = time.perf_counter() - start
content = response.content or ""
usage = getattr(response, "usage_metadata", None)
logger.info(
"[AI] outline model=%s elapsed=%.2fs chars=%s usage=%s",
SMART_MODEL,
elapsed,
len(str(content)),
usage,
)
if content:
return str(content).strip()
except Exception as exc:
logger.error("[AI] Outline generation failed, falling back: %s", exc)
safe_prompt = prompt.strip() or "Document"
return (
"1) Introduction - Summary of the document goals.\n"
"2) Background - Context and key assumptions.\n"
"3) Main Content - Core points and supporting details.\n"
"4) Evidence - Data, examples, or references.\n"
"5) Conclusion - Wrap-up and next steps.\n"
f"Notes: Tailor details to '{safe_prompt}'."
)
def _parse_outline_to_sections(outline_text: str) -> List[Dict[str, str]]:
lines = [
line.strip()
for line in outline_text.split("\n")
if line.strip() and not re.match(r"^(section|details)$", line.strip(), re.IGNORECASE)
]
sections: List[Dict[str, str]] = []
i = 0
while i < len(lines):
cleaned = re.sub(r"^\d+[\).\s-]+", "", lines[i]).strip()
if not cleaned:
i += 1
continue
split = re.split(r"[-:]+", cleaned, maxsplit=1)
if len(split) > 1:
sections.append({"label": split[0].strip() or "Section", "value": split[1].strip()})
i += 1
continue
next_line = lines[i + 1].strip() if i + 1 < len(lines) else ""
if next_line and not re.match(r"^\d+[\).\s-]+", next_line):
sections.append({"label": cleaned, "value": next_line})
i += 2
continue
sections.append({"label": cleaned, "value": ""})
i += 1
return sections
def _extract_fields_from_prompt(prompt: str, fields: List[Dict[str, Any]]) -> List[Dict[str, str]]:
lines = [line.strip() for line in prompt.split("\n") if line.strip()]
kv_pairs: Dict[str, str] = {}
for line in lines:
match = re.match(r"^([^:]{2,40}):\s*(.+)$", line)
if match:
kv_pairs[match.group(1).strip().lower()] = match.group(2).strip()
email_match = re.search(r"[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}", prompt, re.IGNORECASE)
phone_match = re.search(r"(\+?\d[\d\s().-]{7,})", prompt)
date_match = re.search(r"\b\d{1,2}[\/.-]\d{1,2}[\/.-]\d{2,4}\b", prompt)
money_match = re.search(r"\$\s?\d[\d,]*(?:\.\d{2})?", prompt)
filled: List[Dict[str, str]] = []
for field in fields:
label = str(field.get("label", "Field"))
value = str(field.get("value", "") or "")
if value.strip():
filled.append({"label": label, "value": value})
continue
label_lower = label.lower()
for key, val in kv_pairs.items():
if key in label_lower:
value = val
break
if not value and email_match and "email" in label_lower:
value = email_match.group(0)
if not value and phone_match and "phone" in label_lower:
value = phone_match.group(0)
if not value and date_match and ("date" in label_lower or "due" in label_lower):
value = date_match.group(0)
if not value and money_match and ("total" in label_lower or "amount" in label_lower):
value = money_match.group(0)
filled.append({"label": label, "value": value})
return filled
def generate_field_values(
prompt: str,
document_type: str,
fields: List[Dict[str, Any]],
constraints: Optional[Dict[str, Any]] = None,
) -> List[Dict[str, str]]:
if CLIENT_MODE == "langchain":
constraint_text = ""
if constraints:
tone = constraints.get("tone")
audience = constraints.get("audience")
pages = constraints.get("pageCount")
constraint_text = f"Tone: {tone}. Audience: {audience}. Target pages: {pages}."
system_prompt = (
"You are extracting field values from a user prompt.\n"
"Return a JSON array of objects with keys: label, value.\n"
"Only fill values that are explicitly stated or strongly implied.\n"
"If unknown, return an empty string.\n"
f"{constraint_text}\n"
"Output JSON only."
)
messages: List[Dict[str, Any]] = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": f"Document type: {document_type}"},
{"role": "user", "content": f"Prompt:\n{prompt}"},
{"role": "user", "content": f"Fields:\n{json.dumps(fields, ensure_ascii=True)}"},
]
try:
llm = get_chat_model(SMART_MODEL)
if llm:
start = time.perf_counter()
response = llm.invoke(to_lc_messages(messages))
elapsed = time.perf_counter() - start
content = response.content or ""
usage = getattr(response, "usage_metadata", None)
logger.info(
"[AI] field-extract model=%s elapsed=%.2fs chars=%s usage=%s",
SMART_MODEL,
elapsed,
len(str(content)),
usage,
)
if content:
parsed = _extract_json_array(str(content))
if parsed:
return [
{
"label": str(item.get("label", "Field")),
"value": str(item.get("value", "")),
}
for item in parsed
if isinstance(item, dict)
]
except Exception as exc:
logger.error("[AI] Field extraction failed, falling back: %s", exc)
return _extract_fields_from_prompt(prompt, fields)
def _extract_json_array(payload: str) -> Optional[List[Dict[str, Any]]]:
try:
return json.loads(payload)
except json.JSONDecodeError:
match = re.search(r"\[[\s\S]*\]", payload)
if not match:
return None
try:
return json.loads(match.group(0))
except json.JSONDecodeError:
return None
def generate_section_draft(
prompt: str,
document_type: str,
outline_text: str,
constraints: Optional[Dict[str, Any]] = None,
) -> List[Dict[str, str]]:
if CLIENT_MODE == "langchain":
constraint_text = ""
if constraints:
tone = constraints.get("tone")
audience = constraints.get("audience")
pages = constraints.get("pageCount")
constraint_text = f"Tone: {tone}. Audience: {audience}. Target pages: {pages}."
system_prompt = (
"You are generating section content for a document.\n"
"Return a JSON array of objects with keys: label, value.\n"
"Use the provided outline sections as labels; values should be polished draft text.\n"
f"{constraint_text}\n"
"Keep the total length appropriate to the target pages.\n"
"Output JSON only."
)
messages: List[Dict[str, Any]] = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": f"Document type: {document_type}"},
{"role": "user", "content": f"Outline:\n{outline_text}"},
{"role": "user", "content": f"Prompt:\n{prompt}"},
]
try:
llm = get_chat_model(SMART_MODEL)
if llm:
start = time.perf_counter()
response = llm.invoke(to_lc_messages(messages))
elapsed = time.perf_counter() - start
content = response.content or ""
usage = getattr(response, "usage_metadata", None)
logger.info(
"[AI] section-draft model=%s elapsed=%.2fs chars=%s usage=%s",
SMART_MODEL,
elapsed,
len(str(content)),
usage,
)
if content:
parsed = _extract_json_array(str(content))
if parsed:
return [
{
"label": str(item.get("label", "Section")),
"value": str(item.get("value", "")),
}
for item in parsed
if isinstance(item, dict)
]
except Exception as exc:
logger.error("[AI] Section draft generation failed, falling back: %s", exc)
if outline_text.strip():
return _parse_outline_to_sections(outline_text)
fallback_label = "Main Content"
return [{"label": fallback_label, "value": prompt.strip() or "Draft content"}]
def _fallback_template_fill(template_latex: str, outline_text: str, draft_text: Optional[str] = None) -> str:
default_text = draft_text or outline_text or "Details pending."
replacements = {
"TITLE": "Project Overview",
"SUBTITLE": "Executive Summary",
"AUTHOR": "Jane Doe",
"AUTHOR_LIST": "Jane Doe, John Smith",
"AFFILIATIONS": "John Smith Consulting",
"ABSTRACT": default_text,
"KEYWORDS": "keyword1, keyword2, keyword3",
"INTRODUCTION": default_text,
"RELATED_WORK": default_text,
"METHODOLOGY": default_text,
"RESULTS": default_text,
"DISCUSSION": default_text,
"CONCLUSION": default_text,
"REFERENCES": default_text,
"MAIN_TEXT": default_text,
"FIGURES_TABLES": default_text,
"REPORT_TITLE": "Business Report",
"DATE": "2025-01-01",
"EXEC_SUMMARY": default_text,
"BACKGROUND": default_text,
"FINDINGS": default_text,
"RECOMMENDATIONS": default_text,
"APPENDIX": default_text,
"NEWSLETTER_TITLE": "Doe Consulting Monthly",
"TOP_STORY": default_text,
"UPDATES": default_text,
"SPOTLIGHT": default_text,
"FOOTER": "Contact: info@example.com",
"RECIPE_TITLE": "Recipe Title",
"SERVINGS": "Serves 4",
"TIME": "30 minutes",
"INGREDIENTS": "\\\\begin{itemize}\\\\item Ingredient A\\\\item Ingredient B\\\\end{itemize}",
"INSTRUCTIONS": default_text,
"NOTES": "Notes and tips.",
"BUSINESS_NAME": "John Smith Consulting",
"BUSINESS_ADDRESS": "123 Example Street, Example City",
"BUSINESS_CONTACT": "billing@example.com | (555) 000-0000",
"INVOICE_NUMBER": "INV-1001",
"ISSUE_DATE": "2025-01-01",
"DUE_DATE": "2025-01-15",
"CLIENT_NAME": "Doe Corporation",
"CLIENT_ADDRESS": "456 Sample Avenue, Example City",
"CLIENT_CONTACT": "ap@example.com",
"LINE_ITEMS": "Service & 1 & $1000 & $1000 \\\\\\\\",
"SUBTOTAL": "$1000",
"TAXES": "$0",
"TOTAL": "$1000",
"PAYMENT_TERMS": "Net 15",
"PAYMENT_METHODS": "Bank transfer, credit card",
"STUDENT_NAME": "Jane Doe",
"COURSE_NAME": "Business Communications",
"INSTRUCTOR_NAME": "Dr. Rivera",
"ASSIGNMENT_TITLE": "Market Analysis",
"PROMPT": default_text,
"RESPONSE": default_text,
"CHAPTER_ONE_TITLE": "Chapter One",
"CHAPTER_ONE": default_text,
"CHAPTER_TWO_TITLE": "Chapter Two",
"CHAPTER_TWO": default_text,
"PREFACE": default_text,
"PUBLISHER": "Doe Press",
"NAME": "Jane Doe",
"TITLE_PAGE": "Project Overview",
"EMAIL": "jane.doe@example.com",
"PHONE": "(555) 000-0000",
"LOCATION": "Example City, USA",
"SUMMARY": default_text,
"EXPERIENCE": default_text,
"EDUCATION": default_text,
"SKILLS": default_text,
"PROJECTS": default_text,
"SUBJECT": "Subject",
"BODY": default_text,
"RECIPIENT_NAME": "John Smith",
"RECIPIENT_TITLE": "Hiring Manager",
"RECIPIENT_COMPANY": "Doe Corporation",
"RECIPIENT_ADDRESS": "456 Sample Avenue, Example City",
"SENDER_NAME": "Jane Doe",
"SENDER_ADDRESS": "123 Example Street, Example City",
"SENDER_EMAIL": "jane.doe@example.com",
"MONTH_YEAR": "January 2025",
"THEME": "Theme",
"WEEK_ROWS": "1 & 2 & 3 & 4 & 5 & 6 & 7 \\\\\\\\ \\\\hline",
"HEADLINE": "Launch Announcement",
"SUBTEXT": "Introducing our latest release.",
"CALL_TO_ACTION": "Visit example.com to learn more.",
"CONTACT": "contact@example.com",
"EXPERIMENT_TITLE": "Experiment",
"OBJECTIVE": default_text,
"MATERIALS": default_text,
"PROCEDURE": default_text,
"OBSERVATIONS": default_text,
"INSTITUTION": "Doe Institute",
"PRESENTER": "Jane Doe",
"AGENDA": default_text,
"KEY_POINTS": default_text,
"DATA_VISUALS": default_text,
}
def replace(match: re.Match[str]) -> str:
key = match.group(1).strip()
return replacements.get(key, default_text)
return re.sub(r"<<([A-Z0-9_]+)>>", replace, template_latex)
def generate_template_fill_stream(
template_latex: str,
document_type: str,
outline_text: str,
draft_sections: Optional[List[Dict[str, str]]] = None,
constraints: Optional[Dict[str, Any]] = None,
style_profile: Optional[Dict[str, Any]] = None,
):
"""Fill a LaTeX template by replacing placeholders."""
if CLIENT_MODE == "langchain":
constraints_text = ""
if constraints:
tone = constraints.get("tone")
audience = constraints.get("audience")
pages = constraints.get("pageCount")
constraints_text = f"Tone: {tone}. Audience: {audience}. Target pages: {pages}."
style_text = ""
if style_profile:
font = style_profile.get("font_preference")
layout = style_profile.get("layout_preference")
accent = style_profile.get("color_accent")
style_text = f"Style preferences: font={font}, layout={layout}, accent={accent}."
if draft_sections:
constraints_text = f"{constraints_text}\nUse the section content to inform placeholder values."
system_prompt = (
"You are a LaTeX template filler.\n"
"Return the full LaTeX document with placeholders filled.\n"
"Rules:\n"
"1) Only replace placeholders like <<PLACEHOLDER>>.\n"
"2) Do not change any other LaTeX layout/commands.\n"
"3) Output ONLY LaTeX (no markdown).\n"
"4) If you add color, use the accent token name 'accent'.\n"
f"{constraints_text}\n"
f"{style_text}\n"
"Keep the final output within the target page count.\n"
)
messages: List[Dict[str, Any]] = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": f"Document type: {document_type}"},
{"role": "user", "content": f"Outline/context:\n{outline_text}"},
{"role": "user", "content": f"Template:\n{template_latex}"},
]
if draft_sections:
messages.append(
{
"role": "user",
"content": f"Section content (JSON):\n{json.dumps(draft_sections, ensure_ascii=True)}",
}
)
try:
llm = get_chat_model(SMART_MODEL, streaming=True)
if llm:
start = time.perf_counter()
total_chars = 0
chunk_count = 0
first_chunk = None
for chunk in llm.stream(to_lc_messages(messages)):
if chunk.content:
if first_chunk is None:
first_chunk = time.perf_counter()
chunk_count += 1
total_chars += len(str(chunk.content))
yield chunk.content
elapsed = time.perf_counter() - start
logger.info(
"[AI] template-fill-stream model=%s elapsed=%.2fs first_chunk=%.2fs chunks=%s chars=%s",
SMART_MODEL,
elapsed,
(first_chunk - start) if first_chunk else -1.0,
chunk_count,
total_chars,
)
return
except Exception as exc:
logger.error("[AI] Template fill failed, falling back: %s", exc)
draft_text = None
if draft_sections:
draft_text = "\n".join(
f"{section.get('label', 'Section')}: {section.get('value', '')}"
for section in draft_sections
)
filled = _fallback_template_fill(template_latex, outline_text, draft_text)
chunk_size = 200
for i in range(0, len(filled), chunk_size):
yield filled[i : i + chunk_size]
def generate_latex_with_llm(
prompt: str,
history: List[Dict[str, str]],
style_profile: Dict[str, Any],
document_type: str,
template_hint: Optional[str] = None,
current_latex: Optional[str] = None,
structured_brief: Optional[str] = None,
edit_mode: bool = False,
) -> str:
"""Call the LLM (when available) or fall back to deterministic templates."""
if CLIENT_MODE == "langchain":
messages: List[Dict[str, Any]] = [{"role": "system", "content": latex_system_prompt(style_profile, document_type, template_hint)}]
messages.extend(history)
messages.extend(latex_context_messages(template_hint, current_latex, structured_brief))
messages.append({"role": "user", "content": prompt})
try:
logger.info(
"[AI] Using live model=%s doc_type=%s template_hint=%s",
SMART_MODEL,
document_type,
"yes" if template_hint else "no",
)
llm = get_chat_model(SMART_MODEL)
if llm:
start = time.perf_counter()
response = llm.invoke(to_lc_messages(messages))
elapsed = time.perf_counter() - start
content = response.content or ""
usage = getattr(response, "usage_metadata", None)
logger.info(
"[AI] latex-generate model=%s elapsed=%.2fs chars=%s usage=%s",
SMART_MODEL,
elapsed,
len(str(content)),
usage,
)
return content
except Exception as exc:
logger.error("[AI] LangChain generation failed, falling back to mock: %s", exc)
lower_prompt = prompt.lower()
if "invoice" in lower_prompt:
save_user_style("default_user", {"last_doc_type": "invoice"})
details = structured_brief or prompt or "Invoice details provided by user."
return r"""\documentclass{article}
\usepackage[utf8]{inputenc}
\usepackage{geometry}
\geometry{a4paper, margin=1in}
\begin{document}
\begin{center}
{\LARGE \textbf{INVOICE}}\\[0.5cm]
\#1023\\
\today
\end{center}
\section*{Bill To:}
Client Name \\
123 Business Rd.
\section*{Items}
\begin{tabular}{lr}
\textbf{Service} & \textbf{Amount} \\
\hline
% Replace with your line items
Description & \$0.00 \\
Description & \$0.00 \\
\hline
\textbf{Total Due} & \textbf{\$0.00} \\
\end{tabular}
\section*{Notes}
""" + details + r"""
\end{document}"""
if "resume" in lower_prompt or "cv" in lower_prompt:
save_user_style("default_user", {"last_doc_type": "resume"})
details = structured_brief or prompt or "Resume details provided by user."
return r"""\documentclass{article}
\usepackage[utf8]{inputenc}
\usepackage{geometry}
\geometry{a4paper, margin=0.75in}
\begin{document}
\section*{Details}
""" + details + r"""
\end{document}"""
base_doc = r"""\documentclass{article}
\usepackage[utf8]{inputenc}
\usepackage{geometry}
\geometry{a4paper, margin=1in}
\begin{document}
"""
content = structured_brief or prompt or ""
base_doc += r"""\section*{Document}
""" + content + r"""
\end{document}"""
return base_doc
def generate_latex_with_llm_stream(
prompt: str,
history: List[Dict[str, str]],
style_profile: Dict[str, Any],
document_type: str,
template_hint: Optional[str] = None,
current_latex: Optional[str] = None,
structured_brief: Optional[str] = None,
edit_mode: bool = False,
):
"""Stream LaTeX generation from LLM, yielding chunks as they arrive."""
if not STREAMING_ENABLED:
full_latex = generate_latex_with_llm(
prompt, history, style_profile, document_type, template_hint, current_latex, structured_brief, edit_mode=edit_mode
)
chunk_size = 100
for i in range(0, len(full_latex), chunk_size):
yield full_latex[i:i + chunk_size]
return
if CLIENT_MODE == "langchain":
messages: List[Dict[str, Any]] = [{"role": "system", "content": latex_system_prompt(style_profile, document_type, template_hint)}]
messages.extend(history)
messages.extend(latex_context_messages(template_hint, current_latex, structured_brief))
messages.append({"role": "user", "content": prompt})
try:
logger.info(
"[AI] Streaming from model=%s doc_type=%s template_hint=%s",
SMART_MODEL,
document_type,
"yes" if template_hint else "no",
)
llm = get_chat_model(SMART_MODEL, streaming=True)
if llm:
start = time.perf_counter()
total_chars = 0
chunk_count = 0
first_chunk = None
for chunk in llm.stream(to_lc_messages(messages)):
if chunk.content:
if first_chunk is None:
first_chunk = time.perf_counter()
chunk_count += 1
total_chars += len(str(chunk.content))
yield chunk.content
elapsed = time.perf_counter() - start
logger.info(
"[AI] latex-stream model=%s elapsed=%.2fs first_chunk=%.2fs chunks=%s chars=%s",
SMART_MODEL,
elapsed,
(first_chunk - start) if first_chunk else -1.0,
chunk_count,
total_chars,
)
return
except Exception as exc:
logger.error("[AI] LangChain streaming failed, falling back to mock: %s", exc)
# Fallback to non-streaming for mock mode
full_latex = generate_latex_with_llm(
prompt, history, style_profile, document_type, template_hint, current_latex, structured_brief, edit_mode=edit_mode
)
# Simulate streaming by yielding chunks
chunk_size = 100
for i in range(0, len(full_latex), chunk_size):
yield full_latex[i:i + chunk_size]
__all__ = [
"generate_outline_with_llm",
"generate_section_draft",
"generate_field_values",
"generate_latex_with_llm",
"generate_latex_with_llm_stream",
"generate_template_fill_stream",
]
+964
View File
@@ -0,0 +1,964 @@
import os
import mimetypes
import subprocess
import uuid
from pathlib import Path
from datetime import datetime
from typing import Any, Dict, List, Optional, Tuple
import re
import time
import threading
import queue
import urllib.error
import urllib.request
from concurrent.futures import ThreadPoolExecutor
from flask import Flask, jsonify, request, send_file, Response, stream_with_context
from flask_cors import CORS
import json
from ai_generation import (
generate_latex_with_llm,
generate_latex_with_llm_stream,
generate_outline_with_llm,
generate_section_draft,
generate_field_values,
generate_template_fill_stream,
)
from briefs import gather_brief, _preprocess_intent
from config import (
CLIENT_MODE,
SMART_MODEL,
OUTPUT_DIR,
ASSETS_DIR,
TEMPLATE_DIR,
JAVA_BACKEND_URL,
PREVIEW_MAX_INFLIGHT,
get_chat_model,
logger,
)
from langchain_utils import to_lc_messages
from document_types import detect_document_type
from latex_utils import apply_style_overrides, clean_generated_latex
from pdf_utils import compile_latex_to_pdf, render_pdf_to_images
from pdf_text_editor import convert_pdf_to_text_editor_document
from storage import (
load_user_style,
load_user_templates,
load_versions,
save_user_style,
save_user_template,
save_version,
)
from styles import update_style_profile_from_prompt
from vision import vision_layout_from_images
from prompts import pdf_qa_system_prompt
app = Flask(__name__)
CORS(app)
@app.before_request
def log_job_request_sequence() -> None:
job_id = request.headers.get("X-Job-Id")
if not job_id:
return
seq = request.headers.get("X-Job-Seq", "?")
total = request.headers.get("X-Job-Total", "?")
logger.info("[HTTP] job_id=%s req=%s/%s %s %s", job_id, seq, total, request.method, request.path)
def _json_body() -> Dict[str, Any]:
return request.get_json(silent=True) or {}
def _require_ai_enabled() -> Optional[Any]:
if CLIENT_MODE != "langchain":
return jsonify({"error": "AI is disabled. Set OPENAI_API_KEY to enable AI features."}), 503
return None
def _java_url(path: str) -> str:
base = JAVA_BACKEND_URL.rstrip("/")
if not path.startswith("/"):
path = "/" + path
return f"{base}{path}"
def _java_request_json(method: str, path: str, payload: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
url = _java_url(path)
data = None
headers = {"Content-Type": "application/json"}
if payload is not None:
data = json.dumps(payload).encode("utf-8")
req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=10) as resp:
body = resp.read().decode("utf-8")
return json.loads(body) if body else {}
except urllib.error.HTTPError as exc:
detail = exc.read().decode("utf-8") if exc.fp else ""
logger.error("[JAVA] %s %s failed status=%s detail=%s", method, path, exc.code, detail)
raise
def _fetch_ai_session(session_id: str) -> Dict[str, Any]:
return _java_request_json("GET", f"/api/v1/ai/create/internal/sessions/{session_id}")
def _update_ai_session(session_id: str, payload: Dict[str, Any]) -> None:
_java_request_json("POST", f"/api/v1/ai/create/internal/sessions/{session_id}/update", payload)
def _sanitize_doc_type(value: str) -> str:
cleaned = re.sub(r"[^a-zA-Z0-9_]+", "", (value or "").lower())
return cleaned or "miscellaneous"
def _select_template(doc_type: str, template_id: Optional[str]) -> Optional[str]:
safe_doc_type = _sanitize_doc_type(doc_type)
base_dir = Path(TEMPLATE_DIR) / safe_doc_type
if not base_dir.exists() or not base_dir.is_dir():
return None
if template_id:
safe_template = re.sub(r"[^a-zA-Z0-9_-]+", "", template_id)
if safe_template:
candidate = base_dir / f"{safe_template}.tex"
if candidate.exists():
return candidate.read_text(encoding="utf-8", errors="replace")
default_path = base_dir / "default.tex"
if default_path.exists():
return default_path.read_text(encoding="utf-8", errors="replace")
for tex_file in sorted(base_dir.glob("*.tex")):
return tex_file.read_text(encoding="utf-8", errors="replace")
return None
@app.route("/api/intent/check", methods=["POST"])
def intent_check() -> Any:
try:
data = _json_body()
prompt: str = data.get("prompt", "")
history: List[Dict[str, str]] = data.get("conversationHistory") or []
current_latex: Optional[str] = data.get("currentLatex")
current_pdf_url: Optional[str] = data.get("currentPdfUrl")
doc_type = detect_document_type(prompt, current_latex)
intent = _preprocess_intent(prompt, history, bool(current_pdf_url), current_latex)
intent["documentType"] = doc_type
intent["hasPdf"] = bool(current_pdf_url)
return jsonify(intent)
except Exception as exc: # noqa: BLE001
logger.error("[INTENT] intent_check failed: %s", exc, exc_info=True)
return jsonify({"wants_pdf": True, "has_enough_info": True, "allow_makeup": False, "reason": str(exc)}), 500
@app.route("/api/pdf/answer", methods=["POST"])
def pdf_answer() -> Any:
data = _json_body()
pdf_url = data.get("pdfUrl")
question = data.get("question")
if not pdf_url or not question:
return jsonify({"error": "Missing pdfUrl or question"}), 400
filename = os.path.basename(pdf_url.split("?")[0])
if not filename.lower().endswith(".pdf"):
return jsonify({"error": "Invalid pdf file"}), 400
pdf_path = os.path.join(OUTPUT_DIR, filename)
if not os.path.exists(pdf_path):
return jsonify({"error": "PDF not found"}), 404
try:
doc = convert_pdf_to_text_editor_document(pdf_path)
except Exception as exc: # noqa: BLE001
logger.error("[PDF-ANSWER] failed to parse pdf: %s", exc, exc_info=True)
return jsonify({"error": "Failed to read PDF content"}), 500
pages = doc.get("document", {}).get("pages", []) if doc else []
snippets: List[str] = []
for page in pages:
for elem in page.get("textElements", []) or []:
text = elem.get("text")
if text:
snippets.append(str(text))
if not snippets:
return jsonify({"error": "No readable text in PDF"}), 400
# Normalize and limit context
context = " ".join(snippets)
context = " ".join(context.split()) # normalize whitespace
max_context = 10000
if len(context) > max_context:
context = context[:max_context]
# Heuristic helpers
def _sentences(text: str) -> List[str]:
return [s.strip() for s in re.split(r"(?<=[.!?])\s+", text) if s.strip()]
def _heuristic_summary(text: str, limit: int = 480) -> str:
sentences = _sentences(text)
hits = [s for s in sentences if re.search(r"\b(difficult|challenge|problem|issue|hard)\b", s, re.IGNORECASE)]
chosen = hits[:3] if hits else sentences[:3]
summary = " ".join(chosen).strip()
return summary[:limit] + ("" if len(summary) > limit else "")
def _heuristic_first_difficulty(text: str) -> str:
sentences = _sentences(text)
for s in sentences:
if re.search(r"\b(difficult|challenge|problem|issue|hard)\b", s, re.IGNORECASE):
return s
return sentences[0] if sentences else "No difficulty found in PDF text."
if CLIENT_MODE != "langchain":
return jsonify({"error": "PDF Q&A unavailable (no AI client configured)."}), 503
model_name = SMART_MODEL
system_prompt = pdf_qa_system_prompt()
user_prompt = f"Question: {question}\n\nPDF text:\n{context}"
try:
llm = get_chat_model(model_name, max_tokens=220)
if not llm:
return jsonify({"error": "PDF Q&A unavailable (no AI client configured)."}), 503
start = time.perf_counter()
response = llm.invoke(
to_lc_messages(
[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt},
]
)
)
elapsed = time.perf_counter() - start
content = response.content or ""
usage = getattr(response, "usage_metadata", None)
logger.info(
"[PDF-ANSWER] model=%s elapsed=%.2fs chars=%s usage=%s",
model_name,
elapsed,
len(str(content)),
usage,
)
answer = response.content
if not answer or not str(answer).strip():
answer = _heuristic_summary(context)
# If model parrots title/metadata, replace with heuristic
title_like = re.match(r"^why pdfs|^minimalist|^author:", answer.strip(), re.IGNORECASE) if answer else None
normalized_answer = re.sub(r"\s+", " ", answer or "").strip().lower()
normalized_context = re.sub(r"\s+", " ", context).strip().lower()
copied_context = bool(normalized_answer) and normalized_answer in normalized_context
if title_like or copied_context:
answer = _heuristic_summary(context)
return jsonify({"answer": answer, "mode": "model"})
except Exception as exc: # noqa: BLE001
logger.error("[PDF-ANSWER] model failed: %s", exc, exc_info=True)
answer = _heuristic_first_difficulty(context)
return jsonify({"answer": answer, "mode": "heuristic"})
@app.route("/api/generate", methods=["POST"])
def generate() -> Any:
"""Generate LaTeX + PDF in a single call."""
data = _json_body()
user_id = data.get("userId", "default_user")
prompt: str = data.get("prompt", "")
history: List[Dict[str, str]] = data.get("conversationHistory") or []
current_latex: str | None = data.get("currentLatex")
skip_template = bool(data.get("skipTemplate"))
force_new_document: Optional[bool] = data.get("forceNewDocument")
edit_mode = bool(current_latex) and not bool(force_new_document)
latex_source = current_latex if edit_mode else None
style_profile = update_style_profile_from_prompt(user_id, prompt)
doc_type = detect_document_type(prompt, latex_source or current_latex)
brief = gather_brief(doc_type, prompt, history)
if brief.get("needsInfo"):
logger.info("[REQ] brief incomplete doc_type=%s missing=%s", doc_type, brief.get("missing"))
return jsonify(
{
"needsInfo": True,
"message": brief.get("message"),
"missing": brief.get("missing", []),
"collected": brief.get("collected", {}),
"documentType": doc_type,
}
)
templates = load_user_templates(user_id)
template_hint = None if (skip_template or edit_mode) else templates.get(doc_type)
logger.info(
"[REQ] user=%s doc_type=%s template=%s history_len=%s current_latex=%s skip_template=%s edit_mode=%s",
user_id,
doc_type,
"yes" if template_hint else "no",
len(history),
bool(current_latex),
skip_template,
edit_mode,
)
mode = "live" if CLIENT_MODE == "langchain" else "mock"
latex_code_raw = generate_latex_with_llm(
prompt,
history,
style_profile,
doc_type,
template_hint,
latex_source,
brief.get("structured_brief"),
edit_mode=edit_mode,
)
latex_code = apply_style_overrides(clean_generated_latex(latex_code_raw), style_profile)
doc_type = detect_document_type(prompt, latex_code)
save_user_style(user_id, {"last_doc_type": doc_type})
if not skip_template and not edit_mode:
save_user_template(user_id, doc_type, latex_code)
elif skip_template:
logger.info("[TEMPLATE] skip flag set; not persisting template for %s", doc_type)
else:
logger.info("[TEMPLATE] edit mode active; not updating template for %s", doc_type)
template_used = bool(template_hint)
job_id = str(uuid.uuid4())
pdf_path = compile_latex_to_pdf(latex_code, job_id)
if pdf_path and os.path.exists(pdf_path):
pdf_url = f"/output/{job_id}.pdf"
version_entry = {
"id": job_id,
"prompt": prompt,
"documentType": doc_type,
"pdfUrl": pdf_url,
"latex": latex_code,
"createdAt": datetime.utcnow().isoformat() + "Z",
"styleProfile": style_profile,
"templateUsed": template_used,
"editMode": edit_mode,
}
save_version(user_id, version_entry)
logger.info("[OK] job_id=%s doc_type=%s pdf_url=%s", job_id, doc_type, pdf_url)
return jsonify(
{
"latex": latex_code,
"pdfUrl": pdf_url,
"documentType": doc_type,
"message": f"Generated {doc_type} successfully! mode={mode}",
"version": version_entry,
"styleProfile": style_profile,
"mode": mode,
"templateUsed": template_used,
"editingExisting": edit_mode,
}
)
logger.error("[FAIL] job_id=%s doc_type=%s compile_failed", job_id, doc_type)
return jsonify(
{
"error": "LaTeX compilation failed. Check logs.",
"latex": latex_code,
"mode": mode,
"editingExisting": edit_mode,
}
), 500
@app.route("/api/generate_stream", methods=["POST"])
def generate_stream() -> Any:
"""Stream LaTeX generation and compile PDFs incrementally."""
try:
data = _json_body()
user_id = data.get("userId", "default_user")
prompt: str = data.get("prompt", "")
history: List[Dict[str, str]] = data.get("conversationHistory") or []
current_latex: str | None = data.get("currentLatex")
skip_template = bool(data.get("skipTemplate"))
force_new_document: Optional[bool] = data.get("forceNewDocument")
edit_mode = bool(current_latex) and not bool(force_new_document)
latex_source = current_latex if edit_mode else None
style_profile = update_style_profile_from_prompt(user_id, prompt)
doc_type = detect_document_type(prompt, latex_source or current_latex)
brief = gather_brief(doc_type, prompt, history, current_latex, bool(current_latex))
logger.info(
"[STREAM] brief gate doc_type=%s needsInfo=%s missing=%s allowFabrication=%s",
doc_type,
brief.get("needsInfo"),
brief.get("missing"),
brief.get("allowFabrication"),
)
except Exception as exc: # noqa: BLE001
logger.error("[STREAM] Failed to prepare generation request: %s", exc, exc_info=True)
return jsonify({"error": "Failed to start generation", "detail": str(exc)}), 500
if brief.get("needsInfo"):
# Return a normal 200 with guidance so the assistant can ask follow-up questions
return jsonify(
{
"needsInfo": True,
"message": brief.get("message"),
"missing": brief.get("missing", []),
"collected": brief.get("collected", {}),
"documentType": doc_type,
"allowFabrication": brief.get("allowFabrication", False),
}
)
templates = load_user_templates(user_id)
template_hint = None if (skip_template or edit_mode) else templates.get(doc_type)
job_id = str(uuid.uuid4())
accumulated_latex = ""
last_compile_idx = 0
compile_interval = 250 # Compile every ~250 characters for faster previews
compile_time_budget = 2.0 # Or every ~2 seconds, whichever comes first
last_compile_time = time.perf_counter()
last_heartbeat_time = time.perf_counter()
stream_start = time.perf_counter()
first_chunk_time: Optional[float] = None
last_chunk_time: Optional[float] = None
total_chunk_chars = 0
total_chunks = 0
preview_executor = ThreadPoolExecutor(max_workers=2)
preview_tasks: Dict[str, Tuple[Any, int]] = {}
chunk_queue: "queue.Queue[Tuple[str, Optional[str]]]" = queue.Queue()
def stream_latex():
try:
for chunk in generate_latex_with_llm_stream(
prompt,
history,
style_profile,
doc_type,
template_hint,
latex_source,
brief.get("structured_brief"),
edit_mode=edit_mode,
):
chunk_queue.put(("chunk", chunk))
except Exception as exc:
logger.error("[STREAM] LLM streaming failed: %s", exc, exc_info=True)
chunk_queue.put(("error", str(exc)))
finally:
chunk_queue.put(("done", None))
def submit_preview(latex: str, progress: int) -> None:
if len(preview_tasks) >= PREVIEW_MAX_INFLIGHT:
return
preview_job_id = f"{job_id}-preview-{progress}"
def _run_compile() -> Optional[str]:
return compile_latex_to_pdf(latex, preview_job_id, log_errors=False)
fut = preview_executor.submit(_run_compile)
preview_tasks[preview_job_id] = (fut, progress)
def drain_previews():
nonlocal last_compile_idx, last_compile_time
completed = []
for pid, (fut, progress) in list(preview_tasks.items()):
if fut.done():
completed.append(pid)
try:
pdf_path = fut.result()
if pdf_path and os.path.exists(pdf_path):
pdf_url = f"/output/{pid}.pdf"
yield f"data: {json.dumps({'type': 'pdf_update', 'pdfUrl': pdf_url, 'progress': progress})}\n\n"
last_compile_idx = progress
last_compile_time = time.perf_counter()
except Exception as e: # noqa: BLE001
logger.error("[STREAM] Preview compile failed (async): %s", e, exc_info=True)
for pid in completed:
preview_tasks.pop(pid, None)
def generate():
nonlocal accumulated_latex, last_compile_time, last_heartbeat_time
nonlocal total_chunk_chars, total_chunks, first_chunk_time, last_chunk_time
# Send initial metadata
yield f"data: {json.dumps({'type': 'start', 'jobId': job_id, 'documentType': doc_type, 'editingExisting': edit_mode})}\n\n"
streamer = threading.Thread(target=stream_latex, daemon=True)
streamer.start()
try:
while True:
try:
item_type, payload = chunk_queue.get(timeout=0.5)
except queue.Empty:
now = time.perf_counter()
if now - last_heartbeat_time >= 1.0:
yield f"data: {json.dumps({'type': 'heartbeat', 'ts': now})}\n\n"
last_heartbeat_time = now
yield from drain_previews()
continue
if item_type == "chunk":
chunk = payload or ""
accumulated_latex += chunk
total_chunks += 1
total_chunk_chars += len(chunk)
last_chunk_time = time.perf_counter()
if first_chunk_time is None:
first_chunk_time = last_chunk_time
yield f"data: {json.dumps({'type': 'latex_chunk', 'chunk': chunk, 'accumulated': accumulated_latex})}\n\n"
elapsed = time.perf_counter() - last_compile_time
if (len(accumulated_latex) - last_compile_idx >= compile_interval) or elapsed >= compile_time_budget:
compile_latex = accumulated_latex
if "\\begin{document}" in compile_latex and "\\end{document}" not in compile_latex:
if compile_latex.count("\\begin{") > compile_latex.count("\\end{"):
temp_latex = compile_latex
last_begin = compile_latex.rfind("\\begin{")
if last_begin != -1:
env_start = last_begin + len("\\begin{")
env_end = compile_latex.find("}", env_start)
if env_end != -1:
env_name = compile_latex[env_start:env_end]
temp_latex += f"\\end{{{env_name}}}\n"
temp_latex += "\\end{document}\n"
compile_latex = temp_latex
else:
compile_latex += "\\end{document}\n"
if "\\begin{document}" in compile_latex and "\\end{document}" in compile_latex:
submit_preview(compile_latex, len(accumulated_latex))
now = time.perf_counter()
if now - last_heartbeat_time >= 1.0:
yield f"data: {json.dumps({'type': 'heartbeat', 'ts': now})}\n\n"
last_heartbeat_time = now
yield from drain_previews()
elif item_type == "error":
message = payload or "Streaming failed"
yield f"data: {json.dumps({'type': 'error', 'message': message})}\n\n"
break
elif item_type == "done":
break
stream_end = time.perf_counter()
if first_chunk_time is None:
logger.info(
"[STREAM] LLM finished with no chunks job_id=%s elapsed=%.2fs",
job_id,
stream_end - stream_start,
)
else:
logger.info(
"[STREAM] LLM stats job_id=%s chunks=%s chars=%s first_chunk=%.2fs last_chunk=%.2fs elapsed=%.2fs",
job_id,
total_chunks,
total_chunk_chars,
first_chunk_time - stream_start,
(last_chunk_time or stream_end) - stream_start,
stream_end - stream_start,
)
# Final compilation with complete LaTeX
latex_code = apply_style_overrides(clean_generated_latex(accumulated_latex), style_profile)
final_doc_type = detect_document_type(prompt, latex_code)
save_user_style(user_id, {"last_doc_type": final_doc_type})
if not skip_template and not edit_mode:
save_user_template(user_id, final_doc_type, latex_code)
elif skip_template:
logger.info("[TEMPLATE] skip flag set; not persisting template for %s", final_doc_type)
else:
logger.info("[TEMPLATE] edit mode active; not updating template for %s", final_doc_type)
template_used = bool(template_hint)
pdf_path = compile_latex_to_pdf(latex_code, job_id)
if pdf_path and os.path.exists(pdf_path):
pdf_url = f"/output/{job_id}.pdf"
version_entry = {
"id": job_id,
"prompt": prompt,
"documentType": final_doc_type,
"pdfUrl": pdf_url,
"latex": latex_code,
"createdAt": datetime.utcnow().isoformat() + "Z",
"styleProfile": style_profile,
"templateUsed": template_used,
"editMode": edit_mode,
}
save_version(user_id, version_entry)
yield f"data: {json.dumps({'type': 'complete', 'pdfUrl': pdf_url, 'latex': latex_code, 'version': version_entry, 'documentType': final_doc_type, 'templateUsed': template_used, 'styleProfile': style_profile, 'editingExisting': edit_mode})}\n\n"
else:
logger.error("[STREAM] Final PDF compilation failed for job_id=%s", job_id)
yield f"data: {json.dumps({'type': 'error', 'message': 'Final PDF compilation failed'})}\n\n"
except Exception as e: # noqa: BLE001
logger.error("[STREAM] Generation error job_id=%s: %s", job_id, e, exc_info=True)
yield f"data: {json.dumps({'type': 'error', 'message': str(e)})}\n\n"
finally:
try:
preview_executor.shutdown(wait=False, cancel_futures=True)
except Exception:
pass
try:
return Response(
stream_with_context(generate()),
mimetype="text/event-stream",
headers={
"Cache-Control": "no-cache",
"X-Accel-Buffering": "no",
},
)
except Exception as exc: # noqa: BLE001
logger.error("[STREAM] Failed to start response: %s", exc, exc_info=True)
return jsonify({"error": "Unable to start streaming response", "detail": str(exc)}), 500
@app.route("/api/create/sessions/<session_id>/stream", methods=["GET"])
def create_stream(session_id: str) -> Any:
disabled = _require_ai_enabled()
if disabled:
return disabled
phase = (request.args.get("phase") or "outline").strip().lower()
try:
session = _fetch_ai_session(session_id)
except Exception: # noqa: BLE001
return jsonify({"error": "Session not found"}), 404
user_id = session.get("userId", "default_user")
prompt = session.get("promptLatest") or session.get("promptInitial") or ""
doc_type = session.get("docType") or detect_document_type(prompt, None)
template_id = session.get("templateId")
outline_text = session.get("outlineText") or ""
constraints = session.get("outlineConstraints")
if isinstance(constraints, str) and constraints.strip():
try:
constraints = json.loads(constraints)
except json.JSONDecodeError:
constraints = None
draft_sections_raw = session.get("draftSections")
draft_sections = None
if isinstance(draft_sections_raw, list):
draft_sections = draft_sections_raw
elif isinstance(draft_sections_raw, str) and draft_sections_raw.strip():
try:
draft_sections = json.loads(draft_sections_raw)
except json.JSONDecodeError:
draft_sections = None
style_profile = load_user_style(user_id)
def sse(data: Dict[str, Any]) -> str:
return f"data: {json.dumps(data)}\n\n"
def generate():
yield sse({"type": "phase_changed", "phase": phase})
if phase == "outline":
outline = generate_outline_with_llm(prompt, doc_type, constraints)
_update_ai_session(
session_id,
{
"outlineText": outline,
"outlineConstraints": json.dumps(constraints, ensure_ascii=True) if constraints else None,
"docType": doc_type,
"status": "OUTLINE_PENDING",
},
)
yield sse({"type": "outline_ready", "outlineText": outline})
yield sse({"type": "phase_complete", "phase": "outline"})
return
if phase == "draft":
base_outline = outline_text or prompt
sections = generate_section_draft(prompt, doc_type, base_outline, constraints)
_update_ai_session(
session_id,
{
"draftSections": json.dumps(sections, ensure_ascii=True),
"outlineConstraints": json.dumps(constraints, ensure_ascii=True) if constraints else None,
"docType": doc_type,
"status": "DRAFT_READY",
},
)
yield sse({"type": "draft_sections", "sections": sections})
yield sse({"type": "phase_complete", "phase": "draft", "sections": sections})
return
if phase == "polish":
accumulated = ""
template_latex = _select_template(doc_type, template_id)
if template_latex:
for chunk in generate_template_fill_stream(
template_latex,
doc_type,
outline_text or prompt,
draft_sections=draft_sections,
constraints=constraints,
style_profile=style_profile,
):
accumulated += chunk
yield sse({"type": "latex_delta", "phase": "polish", "delta": chunk})
else:
section_text = ""
if draft_sections:
section_text = "\n".join(
f"{section.get('label', 'Section')}: {section.get('value', '')}"
for section in draft_sections
)
constraint_text = ""
if constraints:
tone = constraints.get("tone")
audience = constraints.get("audience")
pages = constraints.get("pageCount")
constraint_text = f"Tone: {tone}. Audience: {audience}. Target pages: {pages}."
polish_prompt = (
f"Create a polished LaTeX document for a {doc_type}.\n"
"Use the provided section content and keep the substance consistent.\n"
f"{constraint_text}\n"
"Keep the final document within the target page count.\n"
)
for chunk in generate_latex_with_llm_stream(
polish_prompt,
[],
style_profile,
doc_type,
None,
None,
section_text or outline_text or prompt,
edit_mode=True,
):
accumulated += chunk
yield sse({"type": "latex_delta", "phase": "polish", "delta": chunk})
accumulated = apply_style_overrides(accumulated, style_profile)
_update_ai_session(
session_id,
{"polishedLatex": accumulated, "docType": doc_type, "status": "POLISHED_READY"},
)
pdf_job_id = f"{session_id}-polished"
pdf_path = compile_latex_to_pdf(accumulated, pdf_job_id, log_errors=False)
if pdf_path and os.path.exists(pdf_path):
pdf_url = f"/output/{pdf_job_id}.pdf"
yield sse({"type": "save_complete", "docId": session_id, "pdfUrl": pdf_url})
yield sse({"type": "phase_complete", "phase": "polish", "latex": accumulated})
return
yield sse({"type": "error", "message": f"Unknown phase: {phase}"})
return Response(
stream_with_context(generate()),
mimetype="text/event-stream",
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
)
@app.route("/api/create/sessions/<session_id>/fields", methods=["POST"])
def fill_fields(session_id: str) -> Any:
disabled = _require_ai_enabled()
if disabled:
return disabled
try:
logger.info("[AI create] fill_fields session_id=%s", session_id)
session = _fetch_ai_session(session_id)
except Exception as exc: # noqa: BLE001
logger.warning("[AI create] fill_fields session lookup failed session_id=%s error=%s", session_id, exc)
return jsonify({"error": "Session not found"}), 404
data = _json_body()
fields = data.get("fields") or []
extra_prompt = data.get("extraPrompt") or ""
if not isinstance(fields, list):
return jsonify({"error": "Fields must be a list"}), 400
prompt = session.get("promptLatest") or session.get("promptInitial") or ""
if extra_prompt:
prompt = f"{prompt}\n{extra_prompt}"
doc_type = session.get("docType") or detect_document_type(prompt, None)
constraints = session.get("outlineConstraints")
if isinstance(constraints, str) and constraints.strip():
try:
constraints = json.loads(constraints)
except json.JSONDecodeError:
constraints = None
filled = generate_field_values(prompt, doc_type, fields, constraints)
return jsonify({"fields": filled})
@app.route("/api/progressive_render", methods=["POST"])
def progressive_render() -> Any:
"""Compile arbitrary LaTeX (partial or masked) for progressive previews."""
data = _json_body()
latex = data.get("latex")
if not latex or not isinstance(latex, str):
return jsonify({"error": "Missing LaTeX payload"}), 400
job_id = data.get("jobId") or str(uuid.uuid4())
pdf_path = compile_latex_to_pdf(latex, job_id)
if pdf_path and os.path.exists(pdf_path):
return jsonify({"pdfUrl": f"/output/{job_id}.pdf"})
return jsonify({"error": "Progressive compilation failed"}), 500
@app.route("/output/<path:filename>", methods=["GET"])
def serve_output_file(filename: str) -> Any:
"""Serve generated PDF files and stored assets."""
file_path = os.path.join(OUTPUT_DIR, filename)
if os.path.exists(file_path):
mime_type, _ = mimetypes.guess_type(file_path)
return send_file(file_path, mimetype=mime_type or "application/octet-stream")
return jsonify({"error": "File not found"}), 404
@app.route("/api/versions/<user_id>", methods=["GET"])
def list_versions(user_id: str) -> Any:
return jsonify({"versions": load_versions(user_id)})
@app.route("/api/style/<user_id>", methods=["GET"])
def get_style(user_id: str) -> Any:
return jsonify({"style": load_user_style(user_id)})
@app.route("/api/style/<user_id>", methods=["POST"])
def update_style(user_id: str) -> Any:
data = _json_body()
if not isinstance(data, dict):
return jsonify({"error": "Style payload must be an object"}), 400
current = load_user_style(user_id) or {}
merged = {**current, **data}
save_user_style(user_id, merged)
return jsonify({"style": merged})
@app.route("/api/style/apply", methods=["POST"])
def apply_style() -> Any:
data = _json_body()
latex = data.get("latex")
style = data.get("style") or {}
if not latex or not isinstance(latex, str):
return jsonify({"error": "Missing LaTeX payload"}), 400
if not isinstance(style, dict):
return jsonify({"error": "Style payload must be an object"}), 400
updated = apply_style_overrides(latex, style)
return jsonify({"latex": updated})
@app.route("/api/import_template", methods=["POST"])
def import_template() -> Any:
"""Accept a PDF upload, extract layout via vision model, and save as a template."""
user_id = request.form.get("userId", "default_user")
doc_type = request.form.get("docType", "document")
file = request.files.get("file")
if not file:
return jsonify({"error": "No file uploaded"}), 400
pdf_bytes = file.read()
images = render_pdf_to_images(pdf_bytes, max_pages=2, dpi=170)
if not images:
return jsonify({"error": "Failed to render PDF"}), 400
layout_latex = vision_layout_from_images(images, doc_type) or ""
if not layout_latex:
layout_latex = f"""\\documentclass{{article}}
\\usepackage[margin=1in]{{geometry}}
\\usepackage{{tabularx}}
\\usepackage{{multicol}}
\\begin{{document}}
% Fallback template for {doc_type}
\\section*{{Title placeholder}}
Body text goes here.
\\end{{document}}
"""
sanitized = clean_generated_latex(layout_latex)
save_user_template(user_id, doc_type, sanitized)
return jsonify({"message": "Template imported", "docType": doc_type, "pages": len(images)})
@app.route("/api/assets/upload", methods=["POST"])
def upload_asset() -> Any:
file = request.files.get("file")
if not file:
return jsonify({"error": "Missing file"}), 400
_, ext = os.path.splitext(file.filename or "")
ext = ext.lower()
if ext not in {".png", ".jpg", ".jpeg", ".gif"}:
return jsonify({"error": "Unsupported file type"}), 400
asset_id = f"{uuid.uuid4().hex}{ext}"
output_path = os.path.join(ASSETS_DIR, asset_id)
os.makedirs(ASSETS_DIR, exist_ok=True)
file.save(output_path)
return jsonify(
{
"assetId": asset_id,
"assetUrl": f"/output/assets/{asset_id}",
"latexPath": f"assets/{asset_id}",
}
)
@app.route("/api/pdf-editor/document", methods=["GET"])
def pdf_editor_document() -> Any:
"""Expose a JSON snapshot of the PDF for rich text editing."""
pdf_url = request.args.get("pdfUrl")
if not pdf_url:
return jsonify({"error": "Missing pdfUrl"}), 400
filename = os.path.basename(pdf_url.split("?")[0])
if not filename:
return jsonify({"error": "Invalid pdf file"}), 400
if not filename.lower().endswith(".pdf"):
return jsonify({"error": "Invalid pdf file"}), 400
pdf_path = os.path.join(OUTPUT_DIR, filename)
if not os.path.exists(pdf_path):
return jsonify({"error": "PDF not found"}), 404
try:
document = convert_pdf_to_text_editor_document(pdf_path)
return jsonify(document)
except FileNotFoundError:
return jsonify({"error": "Conversion failed"}), 500
except subprocess.CalledProcessError as exc:
logger.error("[PDF-EDITOR] Conversion failed: %s", exc)
return jsonify({"error": "Conversion failed"}), 500
except Exception as exc: # noqa: BLE001
logger.error("[PDF-EDITOR] Unexpected conversion failure: %s", exc)
return jsonify({"error": "Conversion failed"}), 500
@app.route("/api/pdf-editor/upload", methods=["POST"])
def pdf_editor_upload() -> Any:
"""Accept an edited PDF and save it so the preview can refresh."""
file = request.files.get("file")
if not file:
return jsonify({"error": "Missing file"}), 400
job_id = str(uuid.uuid4())
filename = f"{job_id}-edited.pdf"
output_path = os.path.join(OUTPUT_DIR, filename)
os.makedirs(OUTPUT_DIR, exist_ok=True)
file.save(output_path)
logger.info("[PDF-EDITOR] uploaded edited PDF job_id=%s -> %s", job_id, filename)
return jsonify({"pdfUrl": f"/output/{filename}"})
@app.route("/health", methods=["GET"])
def health() -> Any:
return jsonify({"status": "ok", "engine": "pdflatex"})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000, debug=True)
@@ -0,0 +1,525 @@
from __future__ import annotations
import re
import json
from typing import Any, Dict, List, Optional
import time
from config import CLIENT_MODE, FAST_MODEL, SMART_MODEL, get_chat_model, logger
from langchain_utils import to_lc_messages
from prompts import brief_missing_info_system_prompt
BRIEF_SCHEMAS: Dict[str, Dict[str, Any]] = {
"resume": {
"field_order": [
"name",
"contact",
"location",
"target_role",
"summary",
"work_history",
"education",
"skills",
"achievements",
"links",
"constraints",
],
"labels": {
"name": ["name", "full name"],
"contact": ["contact", "contact info", "contact information", "email/phone"],
"location": ["location", "city/country"],
"target_role": ["target role", "role", "title", "headline"],
"summary": ["summary", "objective", "about"],
"work_history": ["experience", "work history", "roles"],
"education": ["education", "studies"],
"skills": ["skills", "stack"],
"achievements": ["achievements", "certifications", "awards"],
"links": ["links", "profiles", "linkedin/github"],
"constraints": ["constraints", "tone/length/style"],
},
"questions": {
"name": "What's your name as you'd like it on the page?",
"contact": "How can someone reach you (email/phone)?",
"location": "Where are you based (or remote)?",
"target_role": "What role/title and industry are you aiming for?",
"summary": "Give me a 12 sentence summary about you.",
"work_history": "Recent roles: company, title, dates, location, and a few bullets with impact.",
"education": "Degree(s), school, and graduation year?",
"skills": "Key skills/stack (tech + relevant soft skills)?",
"achievements": "Awards/certifications/major achievements?",
"links": "Any LinkedIn/GitHub/portfolio links?",
"constraints": "Any tone/length constraints (ATS, one-page, etc.)?",
},
"intro": "Hey! To build a strong resume, you can paste your old resume or just dump everything you remember—name, how to reach you, where you're based, what you're aiming for, your roles, education, skills, links. Share whatever you have and I'll work with it.",
},
"invoice": {
"field_order": [
"your_business",
"client",
"issue_date",
"due_date",
"line_items",
"currency",
"payment_terms",
"notes",
"constraints",
],
"labels": {
"your_business": ["your business", "seller", "from"],
"client": ["client", "bill to"],
"issue_date": ["issue date", "invoice date"],
"due_date": ["due date"],
"line_items": ["line items", "services/items"],
"currency": ["currency"],
"payment_terms": ["payment terms"],
"notes": ["notes"],
"constraints": ["constraints", "layout/style"],
},
"questions": {
"your_business": "Who is issuing the invoice (business name + contact)?",
"client": "Who is being billed (name + contact)?",
"issue_date": "Invoice issue date?",
"due_date": "Due date?",
"line_items": "Line items with description, qty, rate, tax (if any)?",
"currency": "Currency?",
"payment_terms": "Payment terms and payment methods?",
"notes": "Notes to include (late fees, thank you, PO #)?",
"constraints": "Branding/layout preferences?",
},
"intro": "I'll draft an accurate invoice if I know who is billing, who is paying, and the line items. Paste an old invoice or list the details.",
},
}
def classify_intent_with_llm(prompt: str, history: List[Dict[str, str]], current_latex: Optional[str], has_pdf: bool) -> Optional[Dict[str, Any]]:
"""
Use a small model to classify intent instead of brittle regex.
Returns a dict like:
{
"documentType": "invoice|resume|contract|letter|report|form|document",
"action": "new|edit|question",
"allowFabrication": bool,
"wantsPdf": bool,
"hasEnoughInfo": bool,
"missingFields": [str],
"notes": str
}
"""
if CLIENT_MODE != "langchain":
logger.info("[INTENT] skip llm classify: client_mode=%s", CLIENT_MODE)
return None
system = (
"You classify user requests about documents. "
"Output strict JSON. "
"documentType must be one of: academic, agenda, brochure, business_card, case_study, checklist, "
"contract, creative, datasheet, document, flyer, invoice, letter, manual, menu, minutes, newsletter, "
"one_pager, poster, presentation, press_release, proposal, recipe, report, resume, timeline, whitepaper. "
"action: 'new' (make/generate), 'edit' (modify existing), 'question' (asking about it). "
"allowFabrication: true if the user invites making up/placeholder/dummy/random details "
"OR asks you to use your knowledge about a fictional/real character (e.g., 'use what you know about James Bond', "
"'make it for agent 007', 'create resume for Sherlock Holmes', etc.). "
"Basically, if they're NOT providing their own personal details and expect you to fill in from common knowledge or imagination, set this to true. "
"wantsPdf: true if they expect/gave permission to generate a PDF. "
"hasEnoughInfo: true if there is enough info to proceed without asking questions (or if allowFabrication is true). "
"missingFields: key details still needed (e.g., for invoice: seller, client, line items; resume: name, contact, work). "
"notes: short free-form note."
)
conversation = [{"role": "system", "content": system}]
# Trim history to keep request small
trimmed_history = history[-6:] if len(history) > 6 else history
for msg in trimmed_history:
if msg.get("content") and msg.get("role") in {"user", "assistant", "system"}:
conversation.append({"role": msg["role"], "content": msg["content"]})
conversation.append({"role": "user", "content": prompt})
try:
llm = get_chat_model(
FAST_MODEL or SMART_MODEL,
max_tokens=800,
model_kwargs={"response_format": {"type": "json_object"}},
)
if not llm:
logger.info("[INTENT] skip llm classify: no LangChain client")
return None
start = time.perf_counter()
response = llm.invoke(to_lc_messages(conversation))
elapsed = time.perf_counter() - start
content = response.content or ""
usage = getattr(response, "usage_metadata", None)
logger.info(
"[INTENT] llm_classify model=%s elapsed=%.2fs chars=%s usage=%s",
FAST_MODEL or SMART_MODEL,
elapsed,
len(str(content)),
usage,
)
content = response.content
if not content:
logger.info("[INTENT] llm_classify empty content")
return None
data = json.loads(content)
# Normalize
doc_type = str(data.get("documentType") or "document").lower()
allowed_types = {
"academic", "agenda", "brochure", "business_card", "case_study", "checklist",
"contract", "creative", "datasheet", "document", "flyer", "invoice", "letter",
"manual", "menu", "minutes", "newsletter", "one_pager", "poster", "presentation",
"press_release", "proposal", "recipe", "report", "resume", "timeline", "whitepaper"
}
if doc_type not in allowed_types:
doc_type = "document"
action = str(data.get("action") or "new").lower()
if action not in {"new", "edit", "question"}:
action = "new"
result = {
"documentType": doc_type,
"action": action,
"allowFabrication": bool(data.get("allowFabrication")),
"wantsPdf": bool(data.get("wantsPdf", True)),
"hasEnoughInfo": bool(data.get("hasEnoughInfo", True)),
"missingFields": data.get("missingFields") or [],
"notes": data.get("notes") or "",
}
logger.info(
"[INTENT] llm_classify doc_type=%s action=%s allowFabrication=%s wantsPdf=%s hasEnoughInfo=%s missing=%s notes=%s",
result["documentType"],
result["action"],
result["allowFabrication"],
result["wantsPdf"],
result["hasEnoughInfo"],
result["missingFields"],
(result["notes"] or "")[:120],
)
return result
except Exception as exc: # noqa: BLE001
logger.error("[INTENT] LLM classify failed: %s", exc, exc_info=True)
return None
def detect_fabrication_opt_in(prompt: str, history: List[Dict[str, str]]) -> bool:
"""
Ask a small model to decide if the user has permitted invention of missing details.
Returns True when the user says to make things up / whatever is fine /
no preference, even if they haven't provided concrete fields.
"""
if CLIENT_MODE != "langchain":
return False
system = (
"Decide if the user has explicitly permitted you to invent or make up missing details. "
"Reply with strict JSON: {\"allowFabrication\": true|false}. "
"Consider any user instruction like 'make it up', 'whatever you want', 'use dummy info', "
"'fabricate the rest', 'fill in anything' as permission. "
"Do not require specific keywords; infer intent from the conversation. "
"If unclear, set allowFabrication to false."
)
conversation = [{"role": "system", "content": system}]
trimmed_history = history[-8:] if len(history) > 8 else history
for msg in trimmed_history:
if msg.get("content") and msg.get("role") in {"user", "assistant", "system"}:
conversation.append({"role": msg["role"], "content": msg["content"]})
conversation.append({"role": "user", "content": prompt})
try:
llm = get_chat_model(
FAST_MODEL or SMART_MODEL,
max_tokens=100,
model_kwargs={"response_format": {"type": "json_object"}},
)
if not llm:
return False
start = time.perf_counter()
response = llm.invoke(to_lc_messages(conversation))
elapsed = time.perf_counter() - start
content = response.content or ""
usage = getattr(response, "usage_metadata", None)
logger.info(
"[INTENT] fabrication-check model=%s elapsed=%.2fs chars=%s usage=%s",
FAST_MODEL or SMART_MODEL,
elapsed,
len(str(content)),
usage,
)
content = response.content
if not content:
return False
data = json.loads(content)
return bool(data.get("allowFabrication"))
except Exception as exc: # noqa: BLE001
logger.error("[INTENT] fabrication opt-in check failed: %s", exc)
return False
def _preprocess_intent(
prompt: str,
history: List[Dict[str, str]],
has_pdf: bool,
current_latex: Optional[str],
) -> Dict[str, Any]:
"""
Lightweight intent classifier used by /api/intent/check.
It is intentionally heuristic-only to avoid extra model calls. The goal is to
decide whether we should proceed with PDF generation and whether it's OK to
fabricate placeholder content when the user explicitly asks for it.
"""
user_texts = [entry.get("content", "") for entry in history if entry.get("role") == "user"]
user_texts.append(prompt or "")
combined_text = " ".join([t for t in user_texts if t]).strip().lower()
llm = classify_intent_with_llm(prompt, history, current_latex, has_pdf)
if llm:
return {
"wants_pdf": llm.get("wantsPdf", True),
"has_enough_info": llm.get("hasEnoughInfo", True),
"allow_makeup": llm.get("allowFabrication", False),
"document_type": llm.get("documentType"),
"missing_fields": llm.get("missingFields", []),
}
# Fallback heuristics (only if no LLM)
avoid_pdf = bool(re.search(r"\b(no pdf|text only|markdown only|dont (make|generate) pdf)\b", combined_text))
wants_pdf = not avoid_pdf or bool(current_latex) or has_pdf
has_meaningful_text = len(combined_text) > 20
return {
"wants_pdf": wants_pdf,
"has_enough_info": bool(current_latex or has_pdf or has_meaningful_text),
"allow_makeup": False,
"document_type": None,
"missing_fields": [],
}
def _extract_structured_fields(text: str, schema: Dict[str, Any]) -> Dict[str, str]:
"""Naively parse user text to pull schema fields."""
found: Dict[str, str] = {}
lower = text.lower()
if schema.get("field_order") == BRIEF_SCHEMAS["resume"]["field_order"]:
work_matches = re.findall(
r"(?:experience|work history|role|company|position)\s*[:\-]\s*(.+?)(?=\n\n|\Z)",
text,
flags=re.IGNORECASE | re.DOTALL,
)
if work_matches:
found["work_history"] = "\n".join(work_matches[:3])
education_matches = re.findall(
r"(?:education|degree)\s*[:\-]\s*(.+?)(?=\n\n|\Z)",
text,
flags=re.IGNORECASE | re.DOTALL,
)
if education_matches:
found["education"] = "\n".join(education_matches[:2])
skills_match = re.search(r"(?:skills|stack)\s*[:\-]\s*(.+)", text, flags=re.IGNORECASE)
if skills_match:
found["skills"] = skills_match.group(1).strip()
for field, labels in schema.get("labels", {}).items():
for label in labels:
pattern = rf"{label}\s*[:\-]\s*(.+?)(?=\n[A-Z][a-zA-Z ]+[:\-]|\Z)"
match = re.search(pattern, text, flags=re.IGNORECASE | re.DOTALL)
if match:
found[field] = match.group(1).strip()
break
if not found.get("summary") and len(lower) < 200:
found["summary"] = text.strip()
return {k: v for k, v in found.items() if v}
def _format_missing_message(
doc_type: str,
schema: Dict[str, Any],
collected: Dict[str, str],
missing: List[str],
preface: Optional[str] = None,
) -> str:
"""Fallback text asking the user for missing fields."""
intro = schema.get("intro") or f"Need a few details to finish your {doc_type}."
lines = [intro]
if preface:
lines.append(preface)
if collected:
lines.append("Already have:")
for field, value in collected.items():
label = schema.get("labels", {}).get(field, [field])[0]
lines.append(f"- {label}: {value}")
if missing:
lines.append("Still need:")
questions = schema.get("questions", {})
for field in missing[:4]:
ask = questions.get(field) or f"{field}?"
lines.append(f"- {ask}")
lines.append("Partial info is fine—share whatever you remember.")
return "\n".join(lines)
def _ai_missing_message(
doc_type: str,
schema: Dict[str, Any],
collected: Dict[str, str],
missing: List[str],
) -> Optional[str]:
"""Let the model craft clarifying questions when available."""
if CLIENT_MODE != "langchain" or not missing:
return None
collected_lines = [f"- {schema.get('labels', {}).get(field, [field])[0]}: {value}" for field, value in collected.items()]
missing_labels = [schema.get("labels", {}).get(field, [field])[0] for field in missing]
user_text = "We already have:\n" + "\n".join(collected_lines) if collected_lines else "We have nothing yet."
user_text += "\nNeed to ask for: " + ", ".join(missing_labels)
if not collected_lines:
user_text += "\nInvite them to paste an old resume or dump all details if they have them."
system_prompt = brief_missing_info_system_prompt(doc_type)
try:
llm = get_chat_model(SMART_MODEL, max_tokens=400)
if not llm:
return None
start = time.perf_counter()
response = llm.invoke(
to_lc_messages(
[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_text},
]
)
)
elapsed = time.perf_counter() - start
content = response.content or ""
usage = getattr(response, "usage_metadata", None)
logger.info(
"[AI] missing-questions model=%s elapsed=%.2fs chars=%s usage=%s",
SMART_MODEL,
elapsed,
len(str(content)),
usage,
)
return response.content
except Exception as exc:
logger.error("[AI] missing-questions failed: %s", exc)
return None
def gather_brief(doc_type: str, prompt: str, history: List[Dict[str, str]], current_latex: Optional[str] = None, has_pdf: bool = False) -> Dict[str, Any]:
"""
Determine whether we have enough structured details to generate without fabricating.
Returns needsInfo + a formatted message when details are missing, or a structured brief.
"""
classifier = classify_intent_with_llm(prompt, history, current_latex, has_pdf)
if classifier:
doc_type = classifier.get("documentType", doc_type)
logger.info(
"[BRIEF] using llm doc_type=%s allowFabrication=%s missing=%s hasEnoughInfo=%s wantsPdf=%s",
doc_type,
classifier.get("allowFabrication"),
classifier.get("missingFields"),
classifier.get("hasEnoughInfo"),
classifier.get("wantsPdf"),
)
else:
logger.info("[BRIEF] llm classifier unavailable, using fallback schema doc_type=%s", doc_type)
schema = BRIEF_SCHEMAS.get(doc_type)
if not schema:
return {"needsInfo": False, "structured_brief": None, "collected": {}, "missing": []}
user_texts = [entry.get("content", "") for entry in history if entry.get("role") == "user"]
user_texts.append(prompt or "")
combined_text = "\n".join(user_texts)
collected = _extract_structured_fields(combined_text, schema)
missing = [field for field in schema.get("field_order", []) if field not in collected]
classifier_has_enough = bool(classifier.get("hasEnoughInfo")) if classifier else True
allow_makeup = bool(classifier.get("allowFabrication")) if classifier else False
if not allow_makeup:
allow_makeup = detect_fabrication_opt_in(prompt, history)
if classifier and classifier.get("missingFields"):
# If the model provided missing fields, respect that list.
missing = classifier.get("missingFields") or missing
# If the user gave no usable content, do not allow fabrication shortcuts.
# Force the flow to ask for the required fields instead of silently proceeding.
missing_all_fields = len(missing) == len(schema.get("field_order", []))
low_signal_request = not collected and len(combined_text.strip()) < 12
if missing_all_fields and low_signal_request:
allow_makeup = False
def has_minimum_resume(data: Dict[str, str]) -> bool:
has_name = bool(data.get("name"))
has_core = any(data.get(key) for key in ["work_history", "education", "skills", "contact", "target_role"])
return has_name and has_core
def has_minimum_invoice(data: Dict[str, str]) -> bool:
has_parties = data.get("your_business") and data.get("client")
has_items = bool(data.get("line_items"))
return bool(has_parties and has_items)
has_minimum = True
if doc_type == "resume":
has_minimum = has_minimum_resume(collected)
elif doc_type == "invoice":
has_minimum = has_minimum_invoice(collected)
# Decide if we must pause to ask for details. Avoid regex "ready" guesses;
# rely on the classifier's allowFabrication flag and collected data.
must_ask_first = bool(missing) and ((not allow_makeup and not has_minimum) or not classifier_has_enough)
if must_ask_first:
preface = None
if doc_type == "resume" and not has_minimum:
preface = (
"I only have a tiny bit so far. I need at least your name plus one of: contact, "
"a role snippet, education, skills, or target role."
)
if doc_type == "invoice" and not has_minimum:
preface = "Need who is billing, who is paying, and the line items so I don't invent details."
logger.info(
"[BRIEF] gating doc_type=%s missing=%s has_minimum=%s allowFabrication=%s",
doc_type,
missing,
has_minimum,
allow_makeup,
)
message = _ai_missing_message(doc_type, schema, collected, missing) or _format_missing_message(
doc_type, schema, collected, missing, preface=preface
)
message += "\nIf you'd like me to invent anything you didn't share, just say so."
return {
"needsInfo": True,
"message": message,
"collected": collected,
"missing": missing,
"allowFabrication": allow_makeup,
}
# If fabrication is allowed but fields are missing, let downstream generation
# know which areas to fill in plausibly.
fabrication_hint = ""
if allow_makeup and missing:
fabrication_hint = "\n\nIf details are absent, invent plausible, clearly fictional details for: " + ", ".join(missing) + "."
structured_lines = []
for field in schema.get("field_order", []):
value = collected.get(field)
if value:
label = schema.get("labels", {}).get(field, [field])[0]
structured_lines.append(f"{label}: {value}")
structured_brief = "\n".join(structured_lines)
if fabrication_hint:
structured_brief = (structured_brief + fabrication_hint).strip()
if combined_text.strip():
structured_brief = (structured_brief + "\n\nRaw user notes:\n" + combined_text).strip()
return {
"needsInfo": False,
"structured_brief": structured_brief or None,
"collected": collected,
"missing": missing,
"allowFabrication": allow_makeup,
}
__all__ = ["gather_brief", "BRIEF_SCHEMAS", "_preprocess_intent"]
@@ -0,0 +1,94 @@
import logging
import os
from typing import Optional
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
OUTPUT_DIR = os.path.join(BASE_DIR, "output")
ASSETS_DIR = os.path.join(OUTPUT_DIR, "assets")
DATA_DIR = os.path.join(BASE_DIR, "data")
TEMPLATE_DIR = os.path.join(BASE_DIR, "templates")
STYLE_DB_PATH = os.path.join(DATA_DIR, "user_styles.json")
TEMPLATE_DB_PATH = os.path.join(DATA_DIR, "user_templates.json")
VERSIONS_DB_PATH = os.path.join(DATA_DIR, "versions.json")
os.makedirs(OUTPUT_DIR, exist_ok=True)
os.makedirs(ASSETS_DIR, exist_ok=True)
os.makedirs(DATA_DIR, exist_ok=True)
os.makedirs(TEMPLATE_DIR, exist_ok=True)
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY")
OPENAI_BASE_URL = os.environ.get("OPENAI_BASE_URL")
JAVA_BACKEND_URL = os.environ.get("JAVA_BACKEND_URL", "http://localhost:8080")
if not OPENAI_API_KEY:
raise RuntimeError("OPENAI_API_KEY is required to start the AI backend.")
# Default to GPT-5.1 for full document generation (smart model).
# Allow override via SMART_MODEL or legacy OPENAI_MODEL.
SMART_MODEL = os.environ.get("SMART_MODEL") or os.environ.get("OPENAI_MODEL") or "gpt-5.1"
# Default to the nano/ultra-fast tier for intent/pre checks (fast model).
# Allow override via FAST_MODEL or legacy FAST_INTENT_MODEL.
FAST_MODEL = os.environ.get("FAST_MODEL") or os.environ.get("FAST_INTENT_MODEL") or "gpt-4.1-nano"
CLIENT_MODE: Optional[str] = None
LANGCHAIN_AVAILABLE = False
_ChatOpenAI = None
STREAMING_ENABLED = os.environ.get("AI_STREAMING", "true").lower() not in {"0", "false", "no"}
if OPENAI_BASE_URL and "ollama" in OPENAI_BASE_URL and "AI_STREAMING" not in os.environ:
STREAMING_ENABLED = False
PREVIEW_MAX_INFLIGHT = int(os.environ.get("AI_PREVIEW_MAX_INFLIGHT", "3"))
if OPENAI_API_KEY:
try:
from langchain_openai import ChatOpenAI # type: ignore
_ChatOpenAI = ChatOpenAI
LANGCHAIN_AVAILABLE = True
CLIENT_MODE = "langchain"
except Exception as client_exc: # pragma: no cover - import guard
logger.warning("LangChain OpenAI init failed: %s", client_exc)
if CLIENT_MODE == "langchain":
logger.info("AI mode: LIVE (fast_model=%s smart_model=%s)", FAST_MODEL, SMART_MODEL)
else:
logger.info("AI mode: MOCK (no OpenAI key or LangChain init failure)")
def get_chat_model(
model_name: str,
streaming: bool = False,
max_tokens: Optional[int] = None,
model_kwargs: Optional[dict] = None,
):
if not LANGCHAIN_AVAILABLE or not _ChatOpenAI:
return None
kwargs = {"model": model_name, "api_key": OPENAI_API_KEY, "streaming": streaming}
if max_tokens is not None:
kwargs["max_tokens"] = max_tokens
if model_kwargs:
kwargs["model_kwargs"] = model_kwargs
return _ChatOpenAI(**kwargs)
__all__ = [
"logger",
"OUTPUT_DIR",
"ASSETS_DIR",
"DATA_DIR",
"TEMPLATE_DIR",
"STYLE_DB_PATH",
"TEMPLATE_DB_PATH",
"VERSIONS_DB_PATH",
"OPENAI_API_KEY",
"OPENAI_BASE_URL",
"JAVA_BACKEND_URL",
"SMART_MODEL",
"CLIENT_MODE",
"LANGCHAIN_AVAILABLE",
"get_chat_model",
"FAST_MODEL",
"STREAMING_ENABLED",
"PREVIEW_MAX_INFLIGHT",
]
@@ -0,0 +1,57 @@
from __future__ import annotations
from typing import List, Tuple
KEYWORDS: List[Tuple[str, str]] = [
("business card", "business_card"),
("business-card", "business_card"),
("card", "business_card"),
("recipe", "recipe"),
("cookbook", "recipe"),
("menu", "menu"),
("flyer", "flyer"),
("brochure", "brochure"),
("poster", "poster"),
("slide", "presentation"),
("deck", "presentation"),
("presentation", "presentation"),
("pitch", "presentation"),
("whitepaper", "whitepaper"),
("datasheet", "datasheet"),
("case study", "case_study"),
("press release", "press_release"),
("agenda", "agenda"),
("minutes", "minutes"),
("checklist", "checklist"),
("newsletter", "newsletter"),
("proposal", "proposal"),
("one-pager", "one_pager"),
("one pager", "one_pager"),
("invoice", "invoice"),
("resume", "resume"),
("cv", "resume"),
("contract", "contract"),
("agreement", "contract"),
("letter", "letter"),
("report", "report"),
("paper", "academic"),
("research", "academic"),
("thesis", "academic"),
("poem", "creative"),
("manual", "manual"),
("timeline", "timeline"),
]
def detect_document_type(prompt: str, latex_code: str | None = None) -> str:
"""Heuristic classifier for document types based on prompt/latex text."""
text = (prompt or "").lower()
latex_text = (latex_code or "").lower()
for keyword, label in KEYWORDS:
if keyword in text or keyword in latex_text:
return label
return "document"
__all__ = ["detect_document_type"]
@@ -0,0 +1,22 @@
from __future__ import annotations
from typing import Any, Dict, List
from langchain_core.messages import AIMessage, HumanMessage, SystemMessage
def to_lc_messages(messages: List[Dict[str, Any]]):
lc_messages = []
for msg in messages:
role = msg.get("role")
content = msg.get("content")
if role == "system":
lc_messages.append(SystemMessage(content=content))
elif role == "assistant":
lc_messages.append(AIMessage(content=content))
else:
lc_messages.append(HumanMessage(content=content))
return lc_messages
__all__ = ["to_lc_messages"]
@@ -0,0 +1,547 @@
from __future__ import annotations
import re
from functools import lru_cache
from typing import List, Optional
ALLOWED_LATEX_PACKAGES = {
"courier",
"graphicx",
"geometry",
"helvet",
"lmodern",
"mathpazo",
"xcolor",
"tabularx",
"paracol",
"multicol",
"longtable",
"setspace",
"enumitem",
"titlesec",
"array",
"inputenc",
"fontenc",
"tikz",
}
def _strip_body_content(body: str) -> str:
"""
Remove user data while keeping layout/structure commands.
Keeps \begin/\end blocks, command scaffolding, and drops plain text.
"""
lines: List[str] = []
for line in body.splitlines():
stripped = line.strip()
if not stripped:
lines.append("")
continue
if stripped.startswith("%"):
continue
if "\\begin" in stripped or "\\end" in stripped:
lines.append(line)
continue
if stripped.startswith("\\"):
line_no_comments = line.split("%", 1)[0]
line_sections = re.sub(
r"(\\(?:section|subsection|subsubsection|paragraph|subparagraph|chapter|part)\*?)\{[^}]*\}",
r"\\1{}",
line_no_comments,
)
line_items = re.sub(r"^\\item.*", r"\\item {}", line_sections)
line_text_cmds = re.sub(
r"\\text(?:bf|it|tt|sc|sf|normal|emph)\{[^}]*\}",
lambda match: match.group(0).split("{")[0] + "{}",
line_items,
)
cleaned = re.sub(r"(?<!\\)[A-Za-z][A-Za-z0-9 ,.;:'\"!?-]*", "", line_text_cmds).strip()
if cleaned:
lines.append(cleaned)
continue
cleaned = re.sub(r"[A-Za-z0-9]+", "", line).strip()
if cleaned:
lines.append(cleaned)
return "\n".join(lines)
def extract_layout_hint(latex_code: str, max_chars: Optional[int] = None) -> str:
"""Keep layout-defining LaTeX while stripping user text."""
if not latex_code:
return ""
preamble, body = "", latex_code
split_doc = latex_code.split(r"\begin{document}", 1)
if len(split_doc) == 2:
preamble, body = split_doc
sanitized_body = _strip_body_content(body)
hint = f"{preamble}\n% --- layout only (data stripped) ---\n{sanitized_body}"
return hint if max_chars is None else hint[:max_chars]
@lru_cache(maxsize=64)
def _word_to_int(word: str) -> Optional[int]:
"""Convert simple English number words to int (0-100)."""
words = {
"zero": 0,
"one": 1,
"two": 2,
"three": 3,
"four": 4,
"five": 5,
"six": 6,
"seven": 7,
"eight": 8,
"nine": 9,
"ten": 10,
"twenty": 20,
"thirty": 30,
"forty": 40,
"fifty": 50,
"sixty": 60,
"seventy": 70,
"eighty": 80,
"ninety": 90,
"hundred": 100,
}
return words.get(word.strip().lower())
def _sanitize_color_mix(match: re.Match[str]) -> str:
token = match.group(1)
if token.isdigit():
val = int(token)
else:
converted = _word_to_int(token)
if converted is None:
digits = "".join(ch for ch in token if ch.isdigit())
val = int(digits) if digits else 80
else:
val = converted
val = max(0, min(100, val))
return f"!{val}!"
def sanitize_latex(latex_code: str) -> str:
"""Normalize invalid xcolor syntax like '!eighty!' to '!80!'."""
if not latex_code:
return latex_code
return re.sub(r"!\s*([A-Za-z0-9]+)\s*!", _sanitize_color_mix, latex_code)
def strip_missing_packages(latex_code: str) -> str:
"""Remove packages unavailable in the runtime environment."""
if not latex_code:
return latex_code
code = latex_code
code = re.sub(r"^\\usepackage\{siunitx\}\s*$", "", code, flags=re.MULTILINE)
code = re.sub(r"\\sisetup\{[^}]*\}", "", code, flags=re.DOTALL)
code = re.sub(r"\\num\{([^}]*)\}", r"\\1", code)
code = re.sub(
r"^\\(setmainfont|setsansfont|setmonofont|newfontfamily)\b.*$",
"",
code,
flags=re.MULTILINE,
)
def _filter_packages(match: re.Match[str]) -> str:
options = match.group(1) or ""
packages = [pkg.strip() for pkg in match.group(2).split(",") if pkg.strip()]
allowed = [pkg for pkg in packages if pkg in ALLOWED_LATEX_PACKAGES]
if not allowed:
return ""
return f"\\usepackage{options}{{{', '.join(allowed)}}}"
return re.sub(
r"^\\usepackage(\[[^\]]*\])?\{([^}]*)\}\s*$",
_filter_packages,
code,
flags=re.MULTILINE,
)
def remove_leading_pagebreaks(latex_code: str) -> str:
"""Strip explicit page breaks at the start of the document body."""
if not latex_code:
return latex_code
parts = latex_code.split(r"\begin{document}", 1)
if len(parts) == 2:
preamble, body = parts
cleaned_body = re.sub(
r"^\s*(\\(newpage|clearpage|pagebreak|vfill)\b\s*)+",
"\n",
body,
flags=re.IGNORECASE | re.MULTILINE,
)
return f"{preamble}\\begin{{document}}{cleaned_body}"
return re.sub(
r"^\s*(\\(newpage|clearpage|pagebreak|vfill)\b\s*)+",
"\n",
latex_code,
flags=re.IGNORECASE | re.MULTILINE,
)
def strip_leading_pagebreaks(latex_code: str) -> str:
"""Drop accidental leading page breaks that cause empty first pages."""
if not latex_code:
return latex_code
parts = latex_code.split(r"\begin{document}", 1)
if len(parts) == 2:
preamble, body = parts
cleaned_body = re.sub(
r"^\s*(\\clearpage|\\newpage|\\pagebreak|\\vfill)+\s*",
"",
body,
flags=re.MULTILINE,
)
return f"{preamble}\\begin{{document}}{cleaned_body}"
return re.sub(
r"^\s*(\\clearpage|\\newpage|\\pagebreak|\\vfill)+\s*",
"",
latex_code,
flags=re.MULTILINE,
)
def fix_tabular_row_endings(latex_code: str) -> str:
"""Ensure tabular environments close rows before \\end{tabular}."""
pattern = re.compile(r"(&[^\n]*)\n\\end{tabular}", re.MULTILINE)
return pattern.sub(r"\\1 \\\\ \n\\end{tabular}", latex_code)
def strip_placeholder_rules(latex_code: str) -> str:
"""Remove placeholder boxes like \\rule/\\colorbox used as fake images."""
code = re.sub(r"\\rule\s*\{\s*[\d\.]+[a-zA-Z]*\s*\}\s*\{\s*[\d\.]+[a-zA-Z]*\s*\}", "", latex_code)
code = re.sub(r"\\fcolorbox\{[^}]*\}\{[^}]*\}\{[^}]*\}", "", code)
code = re.sub(r"\\colorbox\{[^}]*\}\{[^}]*\}", "", code)
# Strip simple tikz pictures that are just boxes/fills
code = re.sub(
r"\\begin\{tikzpicture\}[\s\S]*?\\end\{tikzpicture\}",
"",
code,
flags=re.MULTILINE,
)
return code
def strip_number_grouping_junk(latex_code: str) -> str:
"""Remove stray siunitx options text that may leak into the document body."""
if not latex_code:
return latex_code
# Drop standalone lines/paragraphs that look like siunitx option lists (common when chunks split)
return re.sub(
r"(?im)^\s*,\s*(group-minimum-digits|detect-all|table-number-alignment|round-mode|round-precision)\b.*$",
"",
latex_code,
)
def rebalance_invoice_tables(latex_code: str) -> str:
"""Use wrapped columns for common invoice tables to avoid overflow."""
if not latex_code:
return latex_code
# Legacy 4-col invoices (Item, Desc, Price, Total)
code = latex_code.replace(
r"\\begin{tabularx}{\\textwidth}{@{}l l r r@{}}",
r"\\begin{tabularx}{\\textwidth}{@{}>{\\raggedright\\arraybackslash}p{0.30\\textwidth}>{\\raggedright\\arraybackslash}X>{\\raggedleft\\arraybackslash}p{1.5cm}>{\\raggedleft\\arraybackslash}p{2.3cm}@{}}",
)
# Current 5-col invoices (Item, Description, Qty, Unit, Line Total)
wrapped_invoice_five = (
r"@{}"
r">{\\raggedright\\arraybackslash}p{0.16\\textwidth}"
r">{\\raggedright\\arraybackslash}p{0.50\\textwidth}"
r">{\\raggedleft\\arraybackslash}p{0.09\\textwidth}"
r">{\\raggedleft\\arraybackslash}p{0.12\\textwidth}"
r">{\\raggedleft\\arraybackslash}p{0.13\\textwidth}"
r"@{}"
)
five_col_patterns = [
(
# tabularx with first col l/c, X desc, then three p{} numeric cols (matches default invoice template)
r"(\\begin{tabularx}\{\s*\\textwidth\s*\}\{)\s*@?\{\}?\s*[cl]\s+X\s+p\{[^}]+\}\s+p\{[^}]+\}\s+p\{[^}]+\}\s*@?\{\}?\s*(\})",
r"\1" + wrapped_invoice_five + r"\2",
),
(
# longtable version of the same layout (after upgrades)
r"(\\begin{longtable}\{)\s*@?\{\}?\s*[cl]\s+X\s+p\{[^}]+\}\s+p\{[^}]+\}\s+p\{[^}]+\}\s*@?\{\}?\s*(\})",
r"\1" + wrapped_invoice_five + r"\2",
),
]
for pattern, replacement in five_col_patterns:
code = re.sub(pattern, replacement, code, flags=re.IGNORECASE)
return code
def normalize_tabular_like_begins(latex_code: str) -> str:
"""
Fix common malformed tabular/tabularx/longtable begins where the colspec
is not passed as a braced argument (e.g. `\\begin{tabularx}\\textwidth{...}`
or `\\begin{tabularx}{\\textwidth}\\ItemsColSpec`).
"""
if not latex_code:
return latex_code
code = latex_code
# \begin{tabularx}\textwidth{...} -> \begin{tabularx}{\textwidth}{...}
code = re.sub(
r"\\begin{tabularx}\s*\\textwidth\s*\{([^}]*)\}",
lambda m: f"\\begin{{tabularx}}{{\\textwidth}}{{{m.group(1).strip()}}}",
code,
)
# \begin{tabularx}{\textwidth}\ItemsColSpec -> wrap colspec in braces
code = re.sub(
r"\\begin{tabularx}\s*\{\s*\\textwidth\s*\}\s*\\([A-Za-z@][\w@]*)",
lambda m: f"\\begin{{tabularx}}{{\\textwidth}}{{\\{m.group(1)}}}",
code,
)
# \begin{tabularx}{\textwidth}\colspecliteral -> wrap literal spec
code = re.sub(
r"\\begin{tabularx}\s*\{\s*\\textwidth\s*\}\s*([@A-Za-z].*)",
lambda m: f"\\begin{{tabularx}}{{\\textwidth}}{{{m.group(1).strip()}}}",
code,
)
# \begin{tabular}\colspecliteral OR \begin{longtable}\colspecliteral
def _wrap_simple(env: str, text: str) -> str:
return re.sub(
rf"\\begin{{{env}}}\s*([@A-Za-z].*)",
lambda m: f"\\begin{{{env}}}{{{m.group(1).strip()}}}",
text,
)
code = _wrap_simple("tabular", code)
code = _wrap_simple("longtable", code)
return code
def ensure_longtable_support(latex_code: str) -> str:
"""
Guarantee longtable availability and default centering.
- Injects \\usepackage{longtable} if missing.
- Sets \\LTleft/\\LTright to 0pt so longtable spans the text width without manual centering.
"""
if not latex_code:
return latex_code
code = latex_code
if r"\usepackage{longtable}" not in code:
code = re.sub(
r"(\\documentclass[^\n]*\n)",
r"\1\\usepackage{longtable}\n",
code,
count=1,
)
if r"\setlength\LTleft" not in code:
# Use a lambda so backslashes are treated literally (avoid \L escape errors).
code = re.sub(
r"(\\usepackage\{longtable\}[^\n]*\n)",
lambda m: f"{m.group(1)}\\setlength\\LTleft{{0pt}}\n\\setlength\\LTright{{0pt}}\n",
code,
count=1,
)
return code
def _normalize_alignment_to_wrapped_columns(spec: str) -> str:
"""
Convert simple l/c/r specs to wrapped p-columns that respect text width.
Keeps existing p/m/b/X columns unchanged.
"""
if re.search(r"[pmb]\{|\bX\b", spec):
return spec
cols = [ch for ch in spec if ch in ("l", "c", "r")]
if not cols:
return spec
width = max(0.05, min(0.98, 0.98 / len(cols)))
parts: List[str] = []
for ch in cols:
if ch == "r":
parts.append(r">{\raggedleft\arraybackslash}p{" + f"{width:.3f}\\textwidth" + "}")
else:
parts.append(r">{\raggedright\arraybackslash}p{" + f"{width:.3f}\\textwidth" + "}")
return "@{}" + "".join(parts) + "@{}"
def upgrade_tabular_tables_to_longtable(latex_code: str) -> str:
"""
Replace table+tabular blocks with longtable so large tables break across pages,
stay centered, and repeat headers on each page.
"""
if not latex_code:
return latex_code
pattern = re.compile(
r"\\begin{table}.*?\\begin{tabular}\{([^}]*)\}(.*?)\\end{tabular}.*?\\end{table}",
re.DOTALL,
)
def _build_longtable(match: re.Match[str]) -> str:
align_spec = match.group(1)
body = match.group(2).strip()
normalized_spec = _normalize_alignment_to_wrapped_columns(align_spec)
header_block = ""
body_block = body
hline_split = re.split(r"\\hline", body, maxsplit=1)
if len(hline_split) == 2:
header_block = hline_split[0].strip() + r"\\\hline"
body_block = hline_split[1].lstrip()
else:
first_row_split = re.split(r"\\\\", body, maxsplit=1)
header_block = (first_row_split[0].strip() + r"\\") if first_row_split else ""
body_block = first_row_split[1].lstrip() if len(first_row_split) == 2 else body
header_block = header_block.strip()
body_block = body_block.strip()
return (
"\n\\setlength\\LTleft{0pt}\n"
"\\setlength\\LTright{0pt}\n"
f"\\begin{{longtable}}{{{normalized_spec}}}\n"
f"{header_block}\n"
"\\endfirsthead\n"
f"{header_block}\n"
"\\endhead\n"
f"{body_block}\n"
"\\end{longtable}\n"
)
return pattern.sub(_build_longtable, latex_code)
def clean_generated_latex(latex_code: str) -> str:
"""Apply all sanitizers used both for compilation and template storage."""
return rebalance_invoice_tables(
upgrade_tabular_tables_to_longtable(
ensure_longtable_support(
normalize_tabular_like_begins(
strip_number_grouping_junk(
fix_tabular_row_endings(
strip_leading_pagebreaks(
remove_leading_pagebreaks(
strip_missing_packages(
sanitize_latex(
ensure_full_latex_document(latex_code)
)
)
)
)
)
)
)
)
)
)
def apply_style_overrides(latex_code: str, style_profile: dict) -> str:
"""Apply deterministic font + accent styling without altering layout."""
if not latex_code:
return latex_code
code = latex_code
font = (style_profile or {}).get("font_preference") or ""
accent = (style_profile or {}).get("color_accent") or ""
font_map = {
"serif": ("mathpazo", "\\renewcommand{\\familydefault}{\\rmdefault}"),
"sans": ("helvet", "\\renewcommand{\\familydefault}{\\sfdefault}"),
"helvet": ("helvet", "\\renewcommand{\\familydefault}{\\sfdefault}"),
"mono": ("courier", "\\renewcommand{\\familydefault}{\\ttdefault}"),
"modern": ("lmodern", None),
}
pkg = None
family_cmd = None
if isinstance(font, str):
pkg, family_cmd = font_map.get(font.lower(), (None, None))
if pkg:
code = re.sub(
r"^\\usepackage\{(helvet|mathpazo|lmodern|courier)\}\s*$",
"",
code,
flags=re.MULTILINE,
)
code = re.sub(
r"^\\renewcommand\{\\familydefault\}\{\\(sfdefault|rmdefault|ttdefault)\}\s*$",
"",
code,
flags=re.MULTILINE,
)
def _ensure_package_and_command(text: str) -> str:
if not pkg:
return text
insert = f"\\usepackage{{{pkg}}}\n"
if family_cmd:
insert += family_cmd + "\n"
if r"\begin{document}" in text:
return re.sub(r"(\\begin\{document\})", insert + r"\1", text, count=1)
return insert + text
code = _ensure_package_and_command(code)
if accent:
accent_hex_match = re.fullmatch(r"#?([0-9a-fA-F]{6})", str(accent).strip())
if accent_hex_match:
accent_line = f"\\definecolor{{accent}}{{HTML}}{{{accent_hex_match.group(1).upper()}}}"
else:
accent_name = re.sub(r"[^A-Za-z]+", "", str(accent)) or "blue"
accent_line = f"\\colorlet{{accent}}{{{accent_name}}}"
if re.search(r"^\\definecolor\{accent\}|^\\colorlet\{accent\}", code, flags=re.MULTILINE):
code = re.sub(r"^\\definecolor\{accent\}.*$", accent_line, code, flags=re.MULTILINE)
code = re.sub(r"^\\colorlet\{accent\}.*$", accent_line, code, flags=re.MULTILINE)
else:
needs_xcolor = r"\usepackage{xcolor}" not in code
insert = ""
if needs_xcolor:
insert += "\\usepackage{xcolor}\n"
insert += accent_line + "\n"
if r"\usepackage{xcolor}" in code:
code = re.sub(r"(\\usepackage\{xcolor\}[^\n]*\n)", r"\1" + insert, code, count=1)
elif r"\begin{document}" in code:
code = re.sub(r"(\\begin\{document\})", insert + r"\1", code, count=1)
else:
code = insert + code
return code
def ensure_full_latex_document(text: str) -> str:
"""Trim output to a single LaTeX document starting at \\documentclass and ending at \\end{document}."""
if not text:
return text
start = text.find(r"\documentclass")
end = text.rfind(r"\end{document}")
if start == -1 or end == -1:
return text
end += len(r"\end{document}")
return text[start:end]
__all__ = [
"extract_layout_hint",
"sanitize_latex",
"strip_missing_packages",
"remove_leading_pagebreaks",
"strip_leading_pagebreaks",
"fix_tabular_row_endings",
"rebalance_invoice_tables",
"clean_generated_latex",
"apply_style_overrides",
"ensure_full_latex_document",
]
@@ -0,0 +1,491 @@
from __future__ import annotations
import base64
import os
import re
import subprocess
import tempfile
import xml.etree.ElementTree as ET
from typing import Any, Dict, List, Optional
from config import logger
def _safe_float(value: Optional[str], fallback: float = 0.0) -> float:
"""Convert an attribute value to float while handling bad input."""
try:
if value is None:
return fallback
return float(value)
except (TypeError, ValueError):
return fallback
def _read_image_as_data_url(path: str) -> Optional[str]:
"""Return a data URL for the image if it exists."""
if not os.path.exists(path):
return None
mime = "image/png"
_, ext = os.path.splitext(path)
if ext.lower() in {".jpg", ".jpeg"}:
mime = "image/jpeg"
elif ext.lower() == ".gif":
mime = "image/gif"
try:
with open(path, "rb") as img_handle:
encoded = base64.b64encode(img_handle.read()).decode("ascii")
return f"data:{mime};base64,{encoded}"
except OSError as exc:
logger.warning("[PDF-EDITOR] Failed to read image %s: %s", path, exc)
return None
def _parse_fonts(root: ET.Element) -> List[Dict[str, Any]]:
fonts: List[Dict[str, Any]] = []
for spec in root.findall(".//fontspec"):
font_id = spec.attrib.get("id")
base_name = spec.attrib.get("family")
size = _safe_float(spec.attrib.get("size"), 12.0)
color = spec.attrib.get("color")
name_lower = (base_name or "").lower()
flags = 0
if "bold" in name_lower:
flags |= 0x100 # ForceBold
if "italic" in name_lower or "oblique" in name_lower:
flags |= 0x40 # Italic
fonts.append(
{
"id": font_id,
"uid": font_id,
"baseName": base_name,
"embedded": True,
"program": None,
"programFormat": None,
"webProgram": None,
"webProgramFormat": None,
"pdfProgram": None,
"pdfProgramFormat": None,
"ascent": size,
"descent": -size * 0.25,
"unitsPerEm": max(size, 1),
"standard14Name": None,
"color": color,
"fontDescriptorFlags": flags or None,
}
)
return fonts
def _parse_color_components(color: Optional[str]) -> Optional[List[float]]:
"""Convert a hex/rgb color string into normalized RGB components."""
if not color:
return None
color = color.strip()
hex_match = re.fullmatch(r"#?([0-9a-fA-F]{6})", color)
short_hex_match = re.fullmatch(r"#?([0-9a-fA-F]{3})", color)
rgb_match = re.fullmatch(r"rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)", color, re.IGNORECASE)
if hex_match:
hex_value = hex_match.group(1)
r = int(hex_value[0:2], 16)
g = int(hex_value[2:4], 16)
b = int(hex_value[4:6], 16)
return [r / 255.0, g / 255.0, b / 255.0]
if short_hex_match:
hex_value = short_hex_match.group(1)
r = int(hex_value[0] * 2, 16)
g = int(hex_value[1] * 2, 16)
b = int(hex_value[2] * 2, 16)
return [r / 255.0, g / 255.0, b / 255.0]
if rgb_match:
r = min(max(int(rgb_match.group(1)), 0), 255)
g = min(max(int(rgb_match.group(2)), 0), 255)
b = min(max(int(rgb_match.group(3)), 0), 255)
return [r / 255.0, g / 255.0, b / 255.0]
return None
# --------------------
# Table normalization
# --------------------
def _cluster(values: List[float], tol: float) -> List[List[float]]:
clusters: List[List[float]] = []
for v in sorted(values):
if not clusters or abs(v - clusters[-1][-1]) > tol:
clusters.append([v])
else:
clusters[-1].append(v)
return clusters
def _dedupe_by_xy_text(elements: List[Dict[str, Any]], eps: float = 1.0) -> List[Dict[str, Any]]:
"""
Dedupe using quantized x,y and normalized text (ignores width/height jitter).
Keeps the better scoring text; if equal text, keeps the first.
"""
if not elements:
return elements
def _key(el: Dict[str, Any]) -> tuple[int, int, str]:
x = el.get("x") or 0.0
y = el.get("y") or 0.0
t = (el.get("text") or "").strip().replace("\u00a0", " ")
return (int(round(x / eps)), int(round(y / eps)), t)
def _score_text_global(t: str) -> tuple[int, int, int]:
stripped = t.strip()
has_currency = 1 if any(sym in stripped for sym in ("$", "", "£", "¥")) else 0
non_space = sum(1 for ch in stripped if not ch.isspace())
digits = sum(1 for ch in stripped if ch.isdigit())
return (has_currency, non_space, digits)
deduped: Dict[tuple[int, int, str], Dict[str, Any]] = {}
for el in elements:
key = _key(el)
existing = deduped.get(key)
if existing is None:
deduped[key] = el
continue
t_new = key[2]
t_old = (existing.get("text") or "").strip().replace("\u00a0", " ")
score_new = _score_text_global(t_new)
score_old = _score_text_global(t_old)
if score_new > score_old:
deduped[key] = el
return list(deduped.values())
def _detect_table_region(text_elements: List[Dict[str, Any]], page_width: float) -> Optional[Dict[str, Any]]:
"""
Header-agnostic table detection via x clustering.
Returns dict with anchors, boundaries, observed_left/right, y_min/y_max.
"""
candidates = [
el
for el in text_elements
if el.get("text") not in (None, "")
and isinstance(el.get("x"), (int, float))
and isinstance(el.get("y"), (int, float))
and isinstance(el.get("height"), (int, float))
]
if len(candidates) < 8:
return None
heights = [c["height"] for c in candidates if c.get("height")]
if not heights:
return None
med_h = sorted(heights)[len(heights) // 2]
short_candidates = [c for c in candidates if c["height"] <= med_h * 1.8]
if len(short_candidates) < 8:
return None
x_centers = [c["x"] + (c.get("width") or 0) * 0.5 for c in short_candidates]
x_clusters = _cluster(x_centers, tol=12.0)
x_clusters = [c for c in x_clusters if len(c) >= 3]
if len(x_clusters) < 4:
return None
anchors = [sum(c) / len(c) for c in x_clusters]
anchors.sort()
y_vals = sorted(c["y"] for c in short_candidates)
y_clusters = _cluster(y_vals, tol=3.0)
if len(y_clusters) < 3:
return None
y_clusters.sort(key=lambda c: len(c), reverse=True)
y_min = min(y_clusters[0])
y_max = max(y_clusters[0])
min_anchor = min(anchors)
max_anchor = max(anchors)
PAD = 12.0
band_elems = []
for el in short_candidates:
cx = el["x"] + (el.get("width") or 0) * 0.5
if y_min - PAD <= el["y"] <= y_max + PAD and (min_anchor - PAD) <= cx <= (max_anchor + PAD):
band_elems.append(el)
if not band_elems:
return None
observed_left = min(n["x"] for n in band_elems)
observed_right = max(n["x"] + (n.get("width") or 0) for n in band_elems)
raw_bounds: List[float] = []
for i, ax in enumerate(anchors):
if i == 0:
gap = anchors[1] - anchors[0]
raw_bounds.append(ax - gap * 0.5)
else:
raw_bounds.append((anchors[i - 1] + ax) * 0.5)
gap_last = anchors[-1] - anchors[-2] if len(anchors) > 1 else 40.0
raw_bounds.append(anchors[-1] + gap_last * 0.5)
# translate boundaries to align left edge; no scaling to preserve spacing
delta = observed_left - raw_bounds[0]
boundaries = [b + delta for b in raw_bounds]
info = {
"anchors": anchors,
"boundaries": boundaries,
"observed_left": observed_left,
"observed_right": observed_right,
"y_min": y_min,
"y_max": y_max,
}
if os.getenv("PDF_EDITOR_TABLE_DEBUG"):
logger.debug(
"[PDF-EDITOR] table detected anchors=%s boundaries=%s y=(%.2f, %.2f) span=(%.2f, %.2f)",
anchors,
boundaries,
y_min,
y_max,
observed_left,
observed_right,
)
span = boundaries[-1] - boundaries[0]
if span <= 0 or page_width <= 0:
return None
target_left = max(0.0, (page_width - span) * 0.5)
offset = target_left - boundaries[0]
info.update({"offset": offset, "page_width": page_width})
return info
def _snap_table_elements(text_elements: List[Dict[str, Any]], info: Dict[str, Any]) -> List[Dict[str, Any]]:
anchors = info["anchors"]
base_boundaries = info["boundaries"]
offset = info.get("offset", 0.0)
boundaries = [b + offset for b in base_boundaries]
y_min = info["y_min"]
y_max = info["y_max"]
left = info["observed_left"] + offset
right = info["observed_right"] + offset
def _assign_col(cx: float) -> int:
return min(range(len(anchors)), key=lambda i: abs(anchors[i] - cx))
EPS = 1.0
PAD = 6.0
for el in text_elements:
x = el.get("x")
w = el.get("width") or 0
y = el.get("y")
if not isinstance(x, (int, float)) or not isinstance(y, (int, float)):
continue
cx = x + w * 0.5 + offset
if not (left - PAD <= cx <= right + PAD and y_min - PAD <= y <= y_max + PAD):
continue
col = _assign_col(cx - offset)
col_left = boundaries[col]
col_right = boundaries[col + 1]
new_left = x + offset
new_right = x + w + offset
if new_left < col_left - EPS:
new_left = col_left
if new_right > col_right + EPS:
new_right = col_right
if new_right <= new_left:
mid = (col_left + col_right) * 0.5
new_left = mid - 0.5
new_right = mid + 0.5
el["x"] = new_left
el["width"] = max(1.0, new_right - new_left)
el["textMatrix"] = [1, 0, 0, 1, el["x"], el["y"]]
return text_elements
def _parse_page(page_elem: ET.Element, base_dir: str, font_colors: Dict[str, Optional[str]]) -> Dict[str, Any]:
page_width = _safe_float(page_elem.attrib.get("width"), 612.0)
page_height = _safe_float(page_elem.attrib.get("height"), 792.0)
text_elements: List[Dict[str, Any]] = []
image_elements: List[Dict[str, Any]] = []
# Cluster near-identical text draws on the same baseline and keep the best candidate.
EPS = 1.0
best_by_pos: Dict[tuple[int, int], Dict[str, Any]] = {}
def _q(value: float) -> int:
try:
return int(round(value / EPS))
except Exception:
return 0
def _score_text(t: str) -> tuple[int, int, int]:
stripped = t.strip()
has_currency = 1 if any(sym in stripped for sym in ("$", "", "£", "¥")) else 0
non_space = sum(1 for ch in stripped if not ch.isspace())
digits = sum(1 for ch in stripped if ch.isdigit())
return (has_currency, non_space, digits)
for index, text_elem in enumerate(page_elem.findall("text")):
raw_text = "".join(text_elem.itertext()).replace("\u00A0", " ")
text = raw_text.strip("\n")
left = _safe_float(text_elem.attrib.get("left"))
top = _safe_float(text_elem.attrib.get("top"))
width = _safe_float(text_elem.attrib.get("width"))
height = _safe_float(text_elem.attrib.get("height"))
font_id = text_elem.attrib.get("font")
font_color = font_colors.get(font_id) if font_id else None
fill_components = _parse_color_components(font_color)
candidate = {
"id": f"t-{index}",
"text": text,
"fontId": font_id,
"fontSize": height if height > 0 else None,
"x": left,
"y": page_height - top,
"width": width,
"height": height,
"textMatrix": [1, 0, 0, 1, left, page_height - top],
"fillColor": {"colorSpace": "RGB", "components": fill_components} if fill_components else None,
}
pos_key = (_q(left), _q(page_height - top))
existing = best_by_pos.get(pos_key)
if existing is None:
best_by_pos[pos_key] = candidate
else:
if _score_text(candidate["text"]) > _score_text(existing["text"]):
best_by_pos[pos_key] = candidate
# Optional merge of adjacent runs on the same baseline (e.g., "$" + "38.00")
merged: List[Dict[str, Any]] = []
base_elements = _dedupe_by_xy_text(sorted(best_by_pos.values(), key=lambda i: (i["y"], i["x"])), eps=1.0)
# Sort by baseline (y) then x to make merges stable and ordering deterministic
for item in sorted(base_elements, key=lambda i: (i["y"], i["x"])):
if not merged:
merged.append(item)
continue
prev = merged[-1]
same_line = _q(prev["y"]) == _q(item["y"])
if not same_line:
merged.append(item)
continue
prev_right = prev["x"] + (prev.get("width") or 0)
gap = item["x"] - prev_right
max_h = max(prev.get("height") or 0, item.get("height") or 0)
allowed_gap = max(2.0, 0.25 * max_h)
if gap <= allowed_gap and gap >= -allowed_gap:
# Merge
needs_space = (
prev["text"].strip() != ""
and item["text"].strip() != ""
and not prev["text"].endswith(" ")
and not item["text"].startswith(" ")
and not prev["text"].rstrip().endswith(("$", "", "£", "¥"))
)
merged_text = prev["text"] + (" " if needs_space else "") + item["text"]
new_left = min(prev["x"], item["x"])
new_right = max(prev_right, item["x"] + (item.get("width") or 0))
prev.update(
{
"text": merged_text,
"x": new_left,
"width": new_right - new_left,
"height": max_h,
# keep y and fontId from the left-most run
}
)
else:
merged.append(item)
# Phase A: dedupe by xy+text to remove duplicate draws
deduped = _dedupe_by_xy_text(merged, eps=1.0)
# Phase B: table detection + snapping (header-agnostic)
table_info = _detect_table_region(deduped, page_width)
if table_info:
snapped = _snap_table_elements(deduped, table_info)
text_elements = _dedupe_by_xy_text(snapped, eps=1.0)
else:
text_elements = deduped
for img_index, image_elem in enumerate(page_elem.findall("image")):
left = _safe_float(image_elem.attrib.get("left"))
top = _safe_float(image_elem.attrib.get("top"))
width = _safe_float(image_elem.attrib.get("width"))
height = _safe_float(image_elem.attrib.get("height"))
src = image_elem.attrib.get("src")
image_path = os.path.join(base_dir, src) if src else None
data_url = _read_image_as_data_url(image_path) if image_path else None
image_elements.append(
{
"id": src or f"image-{img_index}",
"objectName": src,
"x": left,
"y": max(page_height - top - height, 0),
"width": width,
"height": height,
"left": left,
"top": top,
"bottom": max(page_height - top, 0),
"right": left + width,
"imageData": data_url,
"imageFormat": os.path.splitext(src)[1][1:] if src else None,
}
)
return {
"width": page_width,
"height": page_height,
"pageNumber": _safe_float(page_elem.attrib.get("number"), 0),
"textElements": text_elements,
"imageElements": image_elements,
}
def convert_pdf_to_text_editor_document(pdf_path: str) -> Dict[str, Any]:
"""Convert a PDF to a JSON payload usable by the PDF text editor."""
if not os.path.exists(pdf_path):
raise FileNotFoundError(pdf_path)
with tempfile.TemporaryDirectory() as tmpdir:
output_base = os.path.join(tmpdir, "doc")
command = [
"pdftohtml",
"-xml",
"-enc",
"UTF-8",
"-nodrm",
"-q",
pdf_path,
output_base,
]
try:
subprocess.run(command, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
except (FileNotFoundError, subprocess.CalledProcessError) as exc:
logger.error("[PDF-EDITOR] pdftohtml failed for %s: %s", pdf_path, exc)
raise
xml_path = f"{output_base}.xml"
if not os.path.exists(xml_path):
raise FileNotFoundError(xml_path)
tree = ET.parse(xml_path)
root = tree.getroot()
fonts = _parse_fonts(root)
font_colors = {font["id"]: font.get("color") for font in fonts if font.get("id")}
pages = [_parse_page(page_elem, tmpdir, font_colors) for page_elem in root.findall("page")]
document: Dict[str, Any] = {
"metadata": {"numberOfPages": len(pages)},
"fonts": fonts,
"pages": pages,
"lazyImages": False,
}
return {"document": document}
__all__ = ["convert_pdf_to_text_editor_document"]
@@ -0,0 +1,147 @@
from __future__ import annotations
import base64
import os
import subprocess
import tempfile
import time
from typing import List, Optional
from config import OUTPUT_DIR, logger
_FONT_SPEC_MARKERS = (
"\\usepackage{fontspec}",
"\\setmainfont",
"\\setsansfont",
"\\setmonofont",
"\\newfontfamily",
)
def _needs_unicode_engine(latex_code: str) -> bool:
return any(marker in latex_code for marker in _FONT_SPEC_MARKERS)
def _run_latex(engine: str, tex_filename: str) -> subprocess.CompletedProcess:
return subprocess.run(
[engine, "-interaction=nonstopmode", "-output-directory", OUTPUT_DIR, tex_filename],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
timeout=30,
cwd=OUTPUT_DIR,
)
def render_pdf_to_images(pdf_bytes: bytes, max_pages: int = 2, dpi: int = 160) -> List[str]:
"""Render the first N pages of a PDF to base64-encoded PNG data URLs."""
images: List[str] = []
with tempfile.TemporaryDirectory() as tmpdir:
pdf_path = os.path.join(tmpdir, "upload.pdf")
with open(pdf_path, "wb") as handle:
handle.write(pdf_bytes)
output_prefix = os.path.join(tmpdir, "page")
try:
subprocess.run(
[
"pdftoppm",
"-png",
"-r",
str(dpi),
"-f",
"1",
"-l",
str(max_pages),
pdf_path,
output_prefix,
],
check=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
timeout=30,
)
except Exception as exc:
logger.error("[IMPORT] pdftoppm failed: %s", exc)
return images
for idx in range(1, max_pages + 1):
img_path = f"{output_prefix}-{idx}.png"
if os.path.exists(img_path):
with open(img_path, "rb") as img_handle:
encoded = base64.b64encode(img_handle.read()).decode("utf-8")
images.append(f"data:image/png;base64,{encoded}")
return images
def compile_latex_to_pdf(
latex_code: str,
job_id: str,
*,
log_errors: bool = True,
raise_on_error: bool = False,
) -> Optional[str]:
"""Compile a LaTeX document and return the PDF path."""
tex_filename = os.path.join(OUTPUT_DIR, f"{job_id}.tex")
pdf_filename = f"{job_id}.pdf"
pdf_path = os.path.join(OUTPUT_DIR, pdf_filename)
with open(tex_filename, "w", encoding="utf-8") as handle:
handle.write(latex_code)
try:
t_start = time.perf_counter()
engine = "xelatex" if _needs_unicode_engine(latex_code) else "pdflatex"
first = _run_latex(engine, tex_filename)
if first.returncode != 0 and engine == "pdflatex":
error_output = first.stderr.decode() or first.stdout.decode()
if "fontspec" in error_output and ("XeTeX" in error_output or "LuaTeX" in error_output):
logger.info("[PDF] pdflatex failed due to fontspec; retrying with xelatex")
engine = "xelatex"
first = _run_latex(engine, tex_filename)
t_first = time.perf_counter()
if first.returncode == 0:
_run_latex(engine, tex_filename)
t_end = time.perf_counter()
if os.path.exists(pdf_path):
logger.info(
"[PDF] compiled job_id=%s -> %s (engine=%s first_pass=%.2fs total=%.2fs)",
job_id,
pdf_filename,
engine,
t_first - t_start,
t_end - t_start,
)
return pdf_path
error_output = first.stderr.decode() or first.stdout.decode()
message = error_output.strip() or f"{engine} failed without stderr output"
if log_errors:
logger.error(
"[PDF] not generated job_id=%s code=%s engine=%s after %.2fs: %s",
job_id,
first.returncode,
engine,
t_first - t_start,
message,
)
if raise_on_error:
raise RuntimeError(message)
return None
except subprocess.TimeoutExpired:
message = f"LaTeX compilation timed out for job_id={job_id}"
if log_errors:
logger.error(message)
if raise_on_error:
raise
return None
except Exception as exc:
if log_errors:
logger.error("LaTeX compilation failed: %s", exc)
if raise_on_error:
raise
return None
__all__ = ["render_pdf_to_images", "compile_latex_to_pdf"]
@@ -0,0 +1,134 @@
from __future__ import annotations
import json
from typing import Dict, List, Optional, Any
# Shared rules
ALLOWED_LATEX_PACKAGES = [
"courier",
"graphicx",
"geometry",
"helvet",
"lmodern",
"mathpazo",
"xcolor",
"tabularx",
"paracol",
"multicol",
"longtable",
"setspace",
"enumitem",
"titlesec",
"array",
"inputenc",
"fontenc",
"tikz",
]
LATEX_RULES = [
r"Output must start with \documentclass.",
r"Output must contain exactly one \begin{document} and one \end{document}.",
r"Do not output anything before \documentclass.",
r"Do not output anything after \end{document}.",
"Do not include commentary, apologies, or markdown fences.",
"If uncertain, prefer simpler LaTeX over complex packages.",
"Only use LaTeX packages from the allowlist in this prompt.",
"Do not use fontspec or custom font commands.",
]
def latex_system_prompt(style_profile: Dict[str, Any], document_type: str, template_hint: Optional[str]) -> str:
safe_style = {
"font_preference": style_profile.get("font_preference", "default"),
"tone": style_profile.get("tone", "professional"),
"color_accent": style_profile.get("color_accent", "blue"),
"layout_preference": style_profile.get("layout_preference", "clean"),
}
return (
"You are a LaTeX document generator for PDFs.\n"
f"User Style Profile (trimmed): {json.dumps(safe_style, sort_keys=True)}\n"
f"Document Type: {document_type}\n"
f"Template Hint present: {'yes' if template_hint else 'no'}\n"
"Rules:\n"
"1) Output ONLY valid LaTeX code.\n"
f"- " + "\n- ".join(LATEX_RULES) + "\n"
"2) Use only the allowlisted packages:\n"
f"- " + "\n- ".join(ALLOWED_LATEX_PACKAGES) + "\n"
f"3) Respect preferred font ({safe_style['font_preference']}), tone ({safe_style['tone']}), and color accent ({safe_style['color_accent']}). Use the accent token name 'accent' (e.g., \\color{{accent}} or \\textcolor{{accent}}{{...}}).\n"
"4) If a template hint is provided, stay close to its layout and styling.\n"
"5) Do NOT add placeholder images or black boxes; omit images entirely unless an explicit path or real image content is provided. Do not use \\rule, tikz, or colored rectangles as image stand-ins.\n"
"6) Return a full compilable document."
)
def latex_context_messages(
template_hint: Optional[str],
current_latex: Optional[str],
structured_brief: Optional[str],
) -> List[Dict[str, str]]:
messages: List[Dict[str, str]] = []
if template_hint:
messages.append(
{
"role": "user",
"content": f"REFERENCE TEMPLATE (keep style/layout, do not copy data):\n---\n{template_hint[:2000]}\n---",
}
)
if current_latex:
messages.append(
{
"role": "user",
"content": f"CURRENT LATEX DRAFT (keep structure, apply edits):\n---\n{current_latex[:2000]}\n---",
}
)
if structured_brief:
messages.append(
{
"role": "user",
"content": (
"Structured details gathered from the user (authoritative; do not invent beyond this):\n"
f"---\n{structured_brief}\n---"
),
}
)
return messages
def pdf_qa_system_prompt() -> str:
return (
"You are a helpful assistant. Read the provided PDF text and answer the user's question.\n"
"Respond with:\n"
"Answer: 24 sentences summarizing the answer from the text.\n"
"Evidence: 13 short quotes/snippets from the provided text (must be exact substrings).\n"
"If the answer is not in the text, say: 'Not found in the provided text.' and give a best-effort summary."
)
def brief_missing_info_system_prompt(doc_type: str) -> str:
return (
f"You are a brief-gathering assistant for generating a {doc_type}.\n"
"Be conversational and concise. Ask at most 3 short questions; no multi-part questions.\n"
"If the user hasn't given much, invite them to paste prior material or dump everything they remember.\n"
"Do not invent data; only ask."
)
def vision_layout_system_prompt() -> str:
return (
"You are a LaTeX layout extractor. Given page images of a PDF, return a LaTeX skeleton matching the layout and styling while blanking user content.\n"
"Do NOT copy any readable text from images. Replace all text with placeholders like TITLE HERE, LOREM, XXXX.\n"
"Infer margins, columns, header/footer, tables. Use \\rule{width}{height} placeholders sized to match blocks.\n"
"Use common packages (geometry, xcolor, tabularx, multicol, paracol, tikz). Replace text with placeholders and output a full compilable document.\n"
"Output ONLY LaTeX."
)
__all__ = [
"latex_system_prompt",
"latex_context_messages",
"pdf_qa_system_prompt",
"brief_missing_info_system_prompt",
"vision_layout_system_prompt",
"LATEX_RULES",
]
@@ -0,0 +1,6 @@
Flask==3.0.0
Flask-CORS==4.0.0
# Use modern OpenAI SDK (v1 interface)
openai>=1.12.0
langchain-core==1.2.5
langchain-openai==1.1.6
@@ -0,0 +1,18 @@
from prompts import (
pdf_qa_system_prompt,
vision_layout_system_prompt,
latex_system_prompt,
)
def main():
assert pdf_qa_system_prompt(), "pdf_qa_system_prompt is empty"
assert "Do NOT copy" in vision_layout_system_prompt(), "vision prompt missing no-copy rule"
latex_prompt = latex_system_prompt({}, "document", None)
assert "\\end{document}" in latex_prompt, "latex prompt missing end document mention"
print("prompts OK")
if __name__ == "__main__":
main()
@@ -0,0 +1,262 @@
import re
import shutil
import subprocess
import tempfile
from pathlib import Path
ROOT_DIR = Path(__file__).resolve().parents[2]
TEMPLATE_ROOT = ROOT_DIR / "backend" / "templates"
FRONTEND_PUBLIC = ROOT_DIR / "frontend" / "public" / "templates"
FRONTEND_CATALOG = ROOT_DIR / "frontend" / "src" / "templateCatalog.ts"
TIMEOUT_SEC = 60
LOREM_SENTENCE = (
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt "
"ut labore et dolore magna aliqua."
)
LOREM_PARAGRAPH = (
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt "
"ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco "
"laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in "
"voluptate velit esse cillum dolore eu fugiat nulla pariatur."
)
LOREM_SHORT = "Lorem ipsum dolor sit amet, consectetur adipiscing elit."
PLACEHOLDER_REPLACEMENTS = {
"TITLE": "Sample Title",
"SUBTITLE": "Sample Subtitle",
"AUTHOR": "Sample Author",
"AUTHOR_LIST": "Sample Author One, Sample Author Two",
"AFFILIATIONS": "Sample Organization",
"ABSTRACT": LOREM_PARAGRAPH,
"KEYWORDS": "keyword1, keyword2, keyword3, keyword4",
"INTRODUCTION": LOREM_PARAGRAPH,
"RELATED_WORK": LOREM_PARAGRAPH,
"METHODOLOGY": LOREM_PARAGRAPH,
"RESULTS": LOREM_PARAGRAPH,
"DISCUSSION": LOREM_PARAGRAPH,
"CONCLUSION": LOREM_SHORT,
"REFERENCES": "Doe, J. (2024). Example Reference. Journal of Examples.",
"MAIN_TEXT": f"{LOREM_PARAGRAPH} {LOREM_PARAGRAPH}",
"FIGURES_TABLES": "Figure 1: Example chart. Table 1: Summary of results.",
"REPORT_TITLE": "Business Report",
"DATE": "2025-01-01",
"EXEC_SUMMARY": LOREM_PARAGRAPH,
"BACKGROUND": LOREM_PARAGRAPH,
"FINDINGS": f"{LOREM_SENTENCE} {LOREM_SENTENCE}",
"RECOMMENDATIONS": "Recommendation 1: Improve efficiency. Recommendation 2: Reduce costs.",
"APPENDIX": LOREM_SHORT,
"NEWSLETTER_TITLE": "Monthly Newsletter",
"TOP_STORY": LOREM_PARAGRAPH,
"UPDATES": f"{LOREM_SENTENCE} {LOREM_SENTENCE}",
"SPOTLIGHT": LOREM_PARAGRAPH,
"FOOTER": "Contact: info@example.com | 123 Main Street",
"RECIPE_TITLE": "Sample Recipe",
"SERVINGS": "Serves 4",
"TIME": "30 minutes",
"INGREDIENTS": r"\begin{itemize}\item Ingredient A\item Ingredient B\item Ingredient C\end{itemize}",
"INSTRUCTIONS": LOREM_PARAGRAPH,
"NOTES": "Notes and tips: adjust seasoning to taste.",
"BUSINESS_NAME": "Your Company",
"BUSINESS_ADDRESS": "123 Main Street, Springfield",
"BUSINESS_CONTACT": "email@example.com | (555) 555-5555",
"INVOICE_NUMBER": "INV-001",
"ISSUE_DATE": "2025-01-01",
"DUE_DATE": "2025-01-15",
"CLIENT_NAME": "Client Name",
"CLIENT_ADDRESS": "456 Client Ave, Metropolis",
"CLIENT_CONTACT": "client@example.com",
"LINE_ITEMS": r"Design Services & 8 & 120 & 960 \\ Consulting & 4 & 150 & 600 \\",
"SUBTOTAL": "1560",
"TAXES": "124.80",
"TOTAL": "1684.80",
"PAYMENT_TERMS": "Net 15",
"PAYMENT_METHODS": "Bank transfer, credit card",
"STUDENT_NAME": "Student Name",
"COURSE_NAME": "Course Name",
"INSTRUCTOR_NAME": "Instructor Name",
"ASSIGNMENT_TITLE": "Assignment Title",
"PROMPT": LOREM_SHORT,
"RESPONSE": LOREM_PARAGRAPH,
"CHAPTER_ONE_TITLE": "Chapter One",
"CHAPTER_ONE": LOREM_PARAGRAPH,
"CHAPTER_TWO_TITLE": "Chapter Two",
"CHAPTER_TWO": LOREM_PARAGRAPH,
"PREFACE": LOREM_SHORT,
"PUBLISHER": "Publisher",
"NAME": "Name",
"EMAIL": "email@example.com",
"PHONE": "(555) 555-5555",
"LOCATION": "City, Country",
"SUMMARY": LOREM_SENTENCE,
"EXPERIENCE": f"{LOREM_SENTENCE} {LOREM_SENTENCE}",
"EDUCATION": "University Name, B.S. in Example Studies",
"SKILLS": "Skills: Analysis, Design, Communication",
"PROJECTS": LOREM_SHORT,
"SUBJECT": "Subject",
"BODY": LOREM_PARAGRAPH,
"RECIPIENT_NAME": "Recipient Name",
"RECIPIENT_TITLE": "Recipient Title",
"RECIPIENT_COMPANY": "Recipient Company",
"RECIPIENT_ADDRESS": "Recipient Address",
"SENDER_NAME": "Sender Name",
"SENDER_ADDRESS": "Sender Address",
"SENDER_EMAIL": "sender@example.com",
"MONTH_YEAR": "January 2025",
"THEME": "Theme",
"WEEK_ROWS": "1 & 2 & 3 & 4 & 5 & 6 & 7 \\\\\\\\ \\\\hline",
"HEADLINE": "Headline",
"SUBTEXT": "Supporting message with a clear benefit.",
"CALL_TO_ACTION": "Call to action",
"CONTACT": "contact@example.com",
"EXPERIMENT_TITLE": "Experiment Title",
"OBJECTIVE": LOREM_SHORT,
"MATERIALS": "Materials list goes here.",
"PROCEDURE": LOREM_PARAGRAPH,
"OBSERVATIONS": LOREM_SHORT,
"INSTITUTION": "Institution",
"PRESENTER": "Presenter",
"AGENDA": "Agenda goes here.",
"KEY_POINTS": "Key points go here.",
"DATA_VISUALS": "Data visuals go here.",
"SUBTITLE": "Subtitle",
"ORGANIZATION": "Organization",
}
def render_template_latex(raw_latex: str) -> str:
def replace(match: re.Match[str]) -> str:
key = match.group(1).strip()
return PLACEHOLDER_REPLACEMENTS.get(key, key.replace("_", " ").title())
return re.sub(r"<<([A-Z0-9_]+)>>", replace, raw_latex)
def find_converter() -> str | None:
if shutil.which("pdftoppm"):
return "pdftoppm"
if shutil.which("magick"):
return "magick"
if shutil.which("convert"):
return "convert"
return None
def pdf_to_jpg(pdf_path: Path, jpg_path: Path, converter: str) -> None:
if converter == "pdftoppm":
subprocess.run(
["pdftoppm", "-jpeg", "-f", "1", "-singlefile", str(pdf_path), str(jpg_path.with_suffix(''))],
check=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
timeout=TIMEOUT_SEC,
)
return
if converter == "magick":
subprocess.run(
["magick", "convert", "-density", "150", str(pdf_path), "-quality", "90", str(jpg_path)],
check=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
timeout=TIMEOUT_SEC,
)
return
subprocess.run(
["convert", "-density", "150", str(pdf_path), "-quality", "90", str(jpg_path)],
check=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
timeout=TIMEOUT_SEC,
)
def main() -> None:
if not shutil.which("pdflatex"):
raise SystemExit("pdflatex not found. Install TeX Live or MikTeX to generate thumbnails.")
converter = find_converter()
if not converter:
raise SystemExit("No PDF-to-image converter found. Install poppler-utils or ImageMagick.")
tex_files = list(TEMPLATE_ROOT.rglob("*.tex"))
if not tex_files:
raise SystemExit(f"No templates found in {TEMPLATE_ROOT}")
FRONTEND_PUBLIC.mkdir(parents=True, exist_ok=True)
catalog: dict[str, list[str]] = {}
failures: list[str] = []
for tex_file in tex_files:
doc_type = tex_file.parent.name
template_id = tex_file.stem
target_dir = FRONTEND_PUBLIC / doc_type
target_dir.mkdir(parents=True, exist_ok=True)
target_jpg = target_dir / f"{template_id}.jpg"
catalog.setdefault(doc_type, [])
if template_id not in catalog[doc_type]:
catalog[doc_type].append(template_id)
with tempfile.TemporaryDirectory() as tmpdir:
tmpdir_path = Path(tmpdir)
rendered = render_template_latex(tex_file.read_text(encoding="ascii"))
tmp_tex = tmpdir_path / "template.tex"
tmp_tex.write_text(rendered, encoding="ascii")
try:
subprocess.run(
["pdflatex", "-interaction=nonstopmode", "-halt-on-error", tmp_tex.name],
check=True,
cwd=tmpdir_path,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
timeout=TIMEOUT_SEC,
)
except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as exc:
failures.append(f"{tex_file}: pdflatex failed ({exc})")
continue
pdf_path = tmpdir_path / "template.pdf"
if not pdf_path.exists():
failures.append(f"{tex_file}: PDF not generated")
continue
try:
pdf_to_jpg(pdf_path, target_jpg, converter)
print(f"Wrote {target_jpg}")
except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as exc:
failures.append(f"{tex_file}: image conversion failed ({exc})")
continue
if failures:
print("\nFailures:")
for failure in failures:
print(f"- {failure}")
raise SystemExit("Template thumbnail generation completed with errors.")
entries = []
for doc_type in sorted(catalog.keys()):
templates = sorted(catalog[doc_type])
if "default" in templates:
templates = ["default"] + [t for t in templates if t != "default"]
entries.append(
f" {{ docType: '{doc_type}', templateCount: {len(templates)}, templates: {templates} }}"
)
FRONTEND_CATALOG.write_text(
"// Auto-generated by generate_template_thumbnails.py\n"
"export type TemplateCatalogEntry = {\n"
" docType: string\n"
" templateCount: number\n"
" templates: string[]\n"
"}\n\n"
"export const templateCatalog: TemplateCatalogEntry[] = [\n"
+ ",\n".join(entries)
+ "\n]\n",
encoding="ascii",
)
print(f"Wrote catalog {FRONTEND_CATALOG}")
if __name__ == "__main__":
main()
@@ -0,0 +1,86 @@
from __future__ import annotations
import json
import os
from typing import Any, Dict, List
from config import STYLE_DB_PATH, TEMPLATE_DB_PATH, VERSIONS_DB_PATH
from latex_utils import clean_generated_latex, extract_layout_hint
def _read_json(path: str) -> Dict[str, Any]:
if not os.path.exists(path):
return {}
try:
with open(path, "r", encoding="utf-8") as handle:
return json.load(handle)
except Exception:
return {}
def _write_json(path: str, data: Dict[str, Any]) -> None:
with open(path, "w", encoding="utf-8") as handle:
json.dump(data, handle, indent=2)
def load_user_style(user_id: str) -> Dict[str, Any]:
"""Load (or bootstrap) an individual user's preferred style."""
data = _read_json(STYLE_DB_PATH)
return data.get(
user_id,
{
"layout_preference": "clean",
"font_preference": "helvet",
"tone": "professional",
"color_accent": "blue",
"last_doc_type": None,
},
)
def save_user_style(user_id: str, style_data: Dict[str, Any]) -> Dict[str, Any]:
"""Persist style preferences for a user."""
all_data = _read_json(STYLE_DB_PATH)
current = all_data.get(user_id, load_user_style(user_id))
current.update(style_data)
all_data[user_id] = current
_write_json(STYLE_DB_PATH, all_data)
return current
def load_user_templates(user_id: str) -> Dict[str, str]:
templates = _read_json(TEMPLATE_DB_PATH)
return templates.get(user_id, {})
def save_user_template(user_id: str, doc_type: str, latex_code: str) -> None:
"""Persist sanitized layout hints per doc type."""
templates = _read_json(TEMPLATE_DB_PATH)
user_templates = templates.get(user_id, {})
sanitized = clean_generated_latex(latex_code)
user_templates[doc_type] = extract_layout_hint(sanitized)
templates[user_id] = user_templates
_write_json(TEMPLATE_DB_PATH, templates)
def load_versions(user_id: str) -> List[Dict[str, Any]]:
data = _read_json(VERSIONS_DB_PATH)
return data.get(user_id, [])
def save_version(user_id: str, entry: Dict[str, Any]) -> None:
data = _read_json(VERSIONS_DB_PATH)
versions = data.get(user_id, [])
versions.insert(0, entry)
data[user_id] = versions[:20]
_write_json(VERSIONS_DB_PATH, data)
__all__ = [
"load_user_style",
"save_user_style",
"load_user_templates",
"save_user_template",
"load_versions",
"save_version",
]
@@ -0,0 +1,33 @@
from __future__ import annotations
from typing import Any, Dict
from storage import load_user_style, save_user_style
def update_style_profile_from_prompt(user_id: str, prompt: str) -> Dict[str, Any]:
"""Simple heuristics to remember color/font/tone preferences from prompt text."""
style = load_user_style(user_id)
lower = (prompt or "").lower()
if "modern" in lower or "minimal" in lower:
style["layout_preference"] = "modern"
style["tone"] = "minimalist"
if "classic" in lower or "formal" in lower:
style["layout_preference"] = "classic"
style["tone"] = "formal"
if "serif" in lower:
style["font_preference"] = "serif"
if "sans" in lower:
style["font_preference"] = "helvet"
if "blue" in lower:
style["color_accent"] = "blue"
if "red" in lower:
style["color_accent"] = "red"
if "green" in lower:
style["color_accent"] = "green"
return save_user_style(user_id, style)
__all__ = ["update_style_profile_from_prompt"]
@@ -0,0 +1,52 @@
from __future__ import annotations
from typing import Any, List, Optional
import time
from config import CLIENT_MODE, SMART_MODEL, get_chat_model, logger
from langchain_utils import to_lc_messages
from prompts import vision_layout_system_prompt
def vision_layout_from_images(image_urls: List[str], doc_type: str) -> Optional[str]:
"""Call the multimodal model to recover a LaTeX skeleton from page images."""
if CLIENT_MODE != "langchain" or not image_urls:
return None
system_prompt = vision_layout_system_prompt()
user_content: List[Any] = [
{"type": "text", "text": f"Extract layout for document type: {doc_type}. Return LaTeX skeleton only."}
]
for url in image_urls:
user_content.append({"type": "image_url", "image_url": {"url": url}})
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_content},
]
try:
logger.info("[IMPORT] vision call pages=%s doc_type=%s", len(image_urls), doc_type)
llm = get_chat_model(SMART_MODEL, max_tokens=2800)
if not llm:
return None
start = time.perf_counter()
response = llm.invoke(to_lc_messages(messages))
elapsed = time.perf_counter() - start
content = response.content or ""
usage = getattr(response, "usage_metadata", None)
logger.info(
"[IMPORT] vision model=%s elapsed=%.2fs chars=%s usage=%s",
SMART_MODEL,
elapsed,
len(str(content)),
usage,
)
return response.content
except Exception as exc:
logger.error("[IMPORT] vision generation failed: %s", exc)
return None
__all__ = ["vision_layout_from_images"]
@@ -0,0 +1,36 @@
module.exports = {
root: true,
env: {
browser: true,
es2021: true,
},
extends: [
'eslint:recommended',
'plugin:react/recommended',
'plugin:react-hooks/recommended',
'plugin:@typescript-eslint/recommended',
'prettier',
],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
ecmaFeatures: {
jsx: true,
},
jsxPragma: null,
},
plugins: ['react', 'react-hooks', '@typescript-eslint'],
settings: {
react: {
version: 'detect',
runtime: 'automatic',
},
},
ignorePatterns: ['dist', 'node_modules'],
rules: {
'react/prop-types': 'off',
'react/react-in-jsx-scope': 'off',
'@typescript-eslint/no-explicit-any': 'off',
},
}
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<title>Stirling - Intelligent Document 1.0</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,34 @@
{
"name": "latex-pdf-generator-frontend",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"lint": "eslint \"src/**/*.{ts,tsx}\"",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@xenova/transformers": "^2.17.0",
"pdfjs-dist": "^4.10.38",
"pdf-lib": "^1.17.1",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/react": "^18.2.43",
"@types/react-dom": "^18.2.17",
"@typescript-eslint/eslint-plugin": "^6.14.0",
"@typescript-eslint/parser": "^6.14.0",
"@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.4.16",
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-react": "^7.34.1",
"eslint-plugin-react-hooks": "^4.6.0",
"postcss": "^8.4.32",
"tailwindcss": "^3.4.0",
"vite": "^5.0.8"
}
}
@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

@@ -0,0 +1,134 @@
import { LandingView } from './components/landing/LandingView'
import { ImportLayoutModal } from './components/modals/ImportLayoutModal'
import { WorkspaceView } from './components/workspace/WorkspaceView'
import { useDocumentWorkflow } from './hooks/useDocumentWorkflow'
import { useSpeechCapture } from './hooks/useSpeechCapture'
function App() {
const workflow = useDocumentWorkflow()
const speech = useSpeechCapture({ appendPrompt: workflow.appendPrompt })
const importModal = (
<ImportLayoutModal
isOpen={workflow.showImportModal}
docType={workflow.importDocType}
onDocTypeChange={workflow.setImportDocType}
onClose={() => workflow.setShowImportModal(false)}
onFileSelected={workflow.handleImportTemplate}
isImporting={workflow.isImporting}
status={workflow.importStatus}
/>
)
if (workflow.view === 'landing') {
return (
<>
<LandingView
prompt={workflow.prompt}
onPromptChange={workflow.setPrompt}
onSubmit={workflow.handleInitialSubmit}
onKeyDown={workflow.handleKeyDown}
uploadedPdfFile={workflow.uploadedPdfFile}
onFileSelect={workflow.setUploadedPdfFile}
isImporting={workflow.isImporting}
onToggleRecording={speech.toggleRecording}
onCancelRecording={speech.cancelRecording}
onAcceptRecording={speech.acceptRecording}
isRecording={speech.isRecording}
whisperStatus={speech.whisperStatus}
waveformHistory={speech.waveformHistory}
docTypes={workflow.docTypes}
templateCounts={workflow.templateCounts}
templateCatalog={workflow.templateCatalog}
selectedDocType={workflow.selectedDocType}
selectedTemplateId={workflow.selectedTemplateId}
templatesForSelected={workflow.templatesForSelected}
isTemplateLoading={workflow.isTemplateLoading}
isTemplatePanelOpen={workflow.isTemplatePanelOpen}
onToggleTemplatePanel={() =>
workflow.setIsTemplatePanelOpen(!workflow.isTemplatePanelOpen)
}
onSelectTemplate={workflow.applyTemplateSelection}
templateThumbnailUrl={workflow.templateThumbnailUrl}
formatDocLabel={(value: string) =>
value
.split('_')
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
.join(' ')
}
/>
{importModal}
</>
)
}
return (
<>
<WorkspaceView
isGenerating={workflow.isGenerating}
isLivePreviewing={workflow.isLivePreviewing}
isStageLoading={workflow.isStageLoading}
prompt={workflow.prompt}
onPromptChange={workflow.setPrompt}
onChatSubmit={workflow.handleChatSubmit}
onKeyDown={workflow.handleKeyDown}
currentDoc={workflow.currentDoc}
onBack={() => workflow.setView('landing')}
stage={workflow.stage}
outlineRows={workflow.outlineRows}
outlineSections={workflow.outlineSections}
excludedFields={workflow.excludedFields}
outlineConstraints={workflow.outlineConstraints}
draftRows={workflow.draftRows}
setOutlineRows={workflow.setOutlineRows}
setOutlineSections={workflow.setOutlineSections}
setExcludedFields={workflow.setExcludedFields}
setOutlineConstraints={workflow.setOutlineConstraints}
setDraftRows={workflow.setDraftRows}
docTypes={workflow.docTypes}
templateCounts={workflow.templateCounts}
selectedDocType={workflow.selectedDocType}
selectedTemplateId={workflow.selectedTemplateId}
templatesForSelected={workflow.templatesForSelected}
isTemplateLoading={workflow.isTemplateLoading}
onSelectTemplate={workflow.applyTemplateSelection}
templateThumbnailUrl={workflow.templateThumbnailUrl}
approveOutline={workflow.approveOutline}
onAiOutline={() => {
workflow.fillFieldsFromAI()
}}
approveDraft={workflow.approveDraft}
saveAndReview={workflow.saveAndReview}
styleDraft={workflow.styleDraft}
setStyleDraft={workflow.setStyleDraft}
applyStyleAndRegenerate={workflow.applyStyleAndRegenerate}
onAddPromptInfo={workflow.addPromptForFields}
onStageSelect={(nextStage) => {
if (workflow.isGenerating || workflow.isStageLoading) return
if (nextStage === 'text' && workflow.stage === 'outline') {
workflow.approveOutline()
return
}
if (nextStage === 'styling' && workflow.stage === 'text') {
workflow.approveDraft()
return
}
if (nextStage === 'review' && workflow.stage === 'styling') {
workflow.saveAndReview()
return
}
workflow.setStage(nextStage)
}}
imagePlaceholdersCount={workflow.imagePlaceholdersCount}
isAssetUploading={workflow.isAssetUploading}
assetError={workflow.assetError}
onAddPlaceholderImage={workflow.addImageToPlaceholders}
onRemovePlaceholders={workflow.stripImagePlaceholders}
onOpenImportTemplate={() => workflow.openImportTemplate(workflow.selectedDocType)}
/>
{importModal}
</>
)
}
export default App
@@ -0,0 +1,360 @@
import { FormEvent, KeyboardEvent, useMemo, useState } from 'react'
import { AudioWaveform } from '../ui/AudioWaveform'
interface LandingViewProps {
prompt: string
onPromptChange: (value: string) => void
onSubmit: (event: FormEvent<HTMLFormElement>) => void
onKeyDown: (event: KeyboardEvent<HTMLTextAreaElement>) => void
uploadedPdfFile: File | null
onFileSelect: (file: File | null) => void
isImporting: boolean
onToggleRecording: () => void
onCancelRecording: () => void
onAcceptRecording: () => void
isRecording: boolean
whisperStatus: string | null
waveformHistory: number[][]
docTypes: string[]
templateCounts: Record<string, number>
templateCatalog: { docType: string; templateCount: number; templates: string[] }[]
selectedDocType: string
selectedTemplateId: string
templatesForSelected: string[]
isTemplateLoading: boolean
isTemplatePanelOpen: boolean
onToggleTemplatePanel: () => void
onSelectTemplate: (docType: string, templateId: string) => void
templateThumbnailUrl: (docType: string, templateId: string) => string
formatDocLabel: (value: string) => string
}
export function LandingView({
prompt,
onPromptChange,
onSubmit,
onKeyDown,
uploadedPdfFile,
onFileSelect,
isImporting,
onToggleRecording,
onCancelRecording,
onAcceptRecording,
isRecording,
whisperStatus,
waveformHistory,
docTypes,
templateCounts,
templateCatalog,
selectedDocType,
selectedTemplateId,
templatesForSelected,
isTemplateLoading,
isTemplatePanelOpen,
onToggleTemplatePanel,
onSelectTemplate,
templateThumbnailUrl,
formatDocLabel,
}: LandingViewProps) {
const [templateSearch, setTemplateSearch] = useState('')
const [activeTemplateTab, setActiveTemplateTab] = useState<'popular' | 'legal' | 'financial' | 'academic' | 'marketing' | 'operations'>('popular')
const [expandedDocType, setExpandedDocType] = useState<string | null>(null)
const [hoveredTemplate, setHoveredTemplate] = useState<{ docType: string; templateId: string; x: number; y: number } | null>(null)
const popularDocTypes = new Set(['cvs_and_resumes', 'invoices', 'cover_letters', 'business_reports', 'presentations'])
const legalDocTypes = new Set(['formal_letters', 'theses'])
const financialDocTypes = new Set(['invoices', 'business_reports', 'calendars'])
const academicDocTypes = new Set(['academic_articles', 'academic_journals', 'assignments', 'theses'])
const marketingDocTypes = new Set(['newsletters', 'signs', 'presentations'])
const operationsDocTypes = new Set(['laboratory_reports', 'laboratory_books', 'business_reports'])
const visibleDocTypes = useMemo(() => {
const filtered = docTypes.filter((docType) =>
formatDocLabel(docType).toLowerCase().includes(templateSearch.toLowerCase()),
)
if (activeTemplateTab === 'legal') {
return filtered.filter((docType) => legalDocTypes.has(docType))
}
if (activeTemplateTab === 'financial') {
return filtered.filter((docType) => financialDocTypes.has(docType))
}
if (activeTemplateTab === 'academic') {
return filtered.filter((docType) => academicDocTypes.has(docType))
}
if (activeTemplateTab === 'marketing') {
return filtered.filter((docType) => marketingDocTypes.has(docType))
}
if (activeTemplateTab === 'operations') {
return filtered.filter((docType) => operationsDocTypes.has(docType))
}
return filtered.filter((docType) => popularDocTypes.has(docType))
}, [
academicDocTypes,
activeTemplateTab,
docTypes,
financialDocTypes,
formatDocLabel,
legalDocTypes,
marketingDocTypes,
operationsDocTypes,
popularDocTypes,
templateSearch,
])
const docTypeIcon = (docType: string) => {
switch (docType) {
case 'cvs_and_resumes':
return '📄'
case 'invoices':
return '🧾'
case 'cover_letters':
return '✉️'
case 'business_reports':
return '📊'
case 'formal_letters':
return '📝'
case 'theses':
return '🎓'
case 'presentations':
return '🖥️'
case 'recipes':
return '🍲'
default:
return '📁'
}
}
return (
<div className="min-h-screen bg-gradient-to-b from-slate-50 to-slate-100 text-slate-900">
<header className="flex items-center justify-between px-8 py-6">
<div className="flex items-center gap-2 text-sm font-semibold text-slate-700">
<span className="text-base">Stirling</span>
</div>
<div className="flex items-center gap-2">
<button className="rounded-full border border-slate-200 px-4 py-1.5 text-xs text-slate-500">
Log in
</button>
<button className="rounded-full bg-slate-900 px-4 py-1.5 text-xs text-white">
Get Stirling free
</button>
</div>
</header>
<main className="flex flex-1 items-center justify-center px-6 pb-16">
<div className="w-full max-w-4xl rounded-[32px] border border-slate-200 bg-white p-10 shadow-lg">
<div className="flex flex-col items-center text-center gap-4">
<div className="text-3xl font-semibold text-slate-900">Stirling PDF</div>
<p className="text-sm text-slate-500">Create any PDF you can imagine with AI</p>
</div>
<form onSubmit={onSubmit} className="mt-10">
<div className="rounded-2xl border border-slate-200 bg-white px-5 py-4 shadow-sm">
{isRecording ? (
<div className="space-y-3">
<AudioWaveform history={waveformHistory} />
<div className="flex items-center justify-between text-xs text-slate-500">
<span>{whisperStatus || 'Listening...'}</span>
<div className="flex gap-2">
<button
type="button"
className="rounded-full border border-slate-200 px-3 py-1"
onClick={onCancelRecording}
>
Cancel
</button>
<button
type="button"
className="rounded-full bg-blue-600 px-3 py-1 text-white"
onClick={onAcceptRecording}
>
Accept
</button>
</div>
</div>
</div>
) : (
<>
<textarea
className="w-full resize-none text-sm text-slate-700 placeholder:text-slate-400 focus:outline-none"
rows={1}
value={prompt}
onChange={(event) => onPromptChange(event.target.value)}
onKeyDown={onKeyDown}
placeholder="Make an invoice for me to bill a client for $1500 in consulting fees"
/>
{uploadedPdfFile && (
<div className="mt-2 text-xs text-emerald-600">{uploadedPdfFile.name}</div>
)}
<div className="mt-4 flex items-center justify-between">
<div className="flex items-center gap-2 relative">
<label
htmlFor="pdf-upload-landing"
className="flex h-8 w-8 items-center justify-center rounded-full border border-slate-200 text-slate-400"
>
+
</label>
<input
id="pdf-upload-landing"
type="file"
accept="application/pdf"
onChange={(event) => onFileSelect(event.target.files?.[0] || null)}
className="hidden"
/>
<button
type="button"
className="rounded-full border border-slate-200 px-3 py-1 text-xs text-slate-500"
onClick={onToggleTemplatePanel}
>
Template
</button>
<span className="text-xs text-slate-400">
{formatDocLabel(selectedDocType)} · {formatDocLabel(selectedTemplateId)}
</span>
{isTemplatePanelOpen && (
<div className="absolute left-2 top-10 w-80 rounded-2xl border border-slate-200 bg-white p-4 shadow-lg z-50">
<div className="absolute -top-2 left-6 h-3 w-3 rotate-45 border border-slate-200 bg-white" />
<div className="space-y-3">
<div className="flex items-center gap-2 rounded-full border border-slate-200 px-3 py-2 text-xs text-slate-500">
<span>🔎</span>
<input
className="w-full bg-transparent text-sm text-slate-600 focus:outline-none"
placeholder="Search templates..."
value={templateSearch}
onChange={(event) => setTemplateSearch(event.target.value)}
/>
</div>
<div className="flex gap-2 overflow-x-auto pb-1">
{[
{ id: 'popular', label: 'Popular' },
{ id: 'legal', label: 'Legal' },
{ id: 'financial', label: 'Financial' },
{ id: 'academic', label: 'Academic' },
{ id: 'marketing', label: 'Marketing' },
{ id: 'operations', label: 'Operations' },
].map((tab) => (
<button
type="button"
key={tab.id}
onClick={() => setActiveTemplateTab(tab.id as typeof activeTemplateTab)}
className={`rounded-full px-3 py-1 text-xs ${
activeTemplateTab === tab.id
? 'bg-blue-100 text-blue-700'
: 'bg-slate-100 text-slate-500'
}`}
>
{tab.label}
</button>
))}
</div>
</div>
<div className="mt-4 space-y-2 max-h-64 overflow-y-auto pr-1 relative">
{visibleDocTypes.map((docType) => {
const isExpanded = expandedDocType === docType
const templates =
templateCatalog.find((entry) => entry.docType === docType)?.templates ||
(docType === selectedDocType ? templatesForSelected : ['default'])
return (
<div key={docType} className="rounded-lg border border-slate-200 bg-white">
<div
className="flex w-full items-center justify-between px-3 py-2 text-sm text-slate-700"
>
<button
type="button"
className="flex flex-1 items-center gap-2 text-left"
onClick={() => onSelectTemplate(docType, 'default')}
>
<span>{docTypeIcon(docType)}</span>
<span>{formatDocLabel(docType)}</span>
</button>
<button
type="button"
className="text-slate-400 px-2"
onClick={() => setExpandedDocType(isExpanded ? null : docType)}
>
{isExpanded ? '▾' : '▸'}
</button>
</div>
{isExpanded && (
<div className="border-t border-slate-200 px-3 py-2 space-y-2">
{(templates || ['default']).map((templateId) => (
<button
type="button"
key={`${docType}-${templateId}`}
onClick={() => onSelectTemplate(docType, templateId)}
className={`flex w-full items-center justify-between rounded-md px-2 py-1 text-sm ${
selectedDocType === docType && selectedTemplateId === templateId
? 'bg-blue-50 text-blue-700'
: 'text-slate-600'
}`}
onMouseMove={(event) =>
setHoveredTemplate({
docType,
templateId,
x: event.clientX,
y: event.clientY,
})
}
onMouseLeave={() => setHoveredTemplate(null)}
>
<span>{formatDocLabel(templateId)}</span>
<span className="text-xs text-slate-400">Select</span>
</button>
))}
</div>
)}
</div>
)
})}
{!visibleDocTypes.length && (
<div className="text-sm text-slate-400">No templates found.</div>
)}
{hoveredTemplate && (
<div
className="fixed z-50 w-36 rounded-lg border border-slate-200 bg-white shadow-lg p-2"
style={{ left: hoveredTemplate.x + 12, top: hoveredTemplate.y + 12 }}
>
<div className="text-[10px] uppercase tracking-wide text-slate-400 mb-2">
Preview
</div>
<div className="w-full bg-slate-50 rounded-md overflow-hidden" style={{ aspectRatio: '210 / 297' }}>
<img
src={templateThumbnailUrl(hoveredTemplate.docType, hoveredTemplate.templateId)}
alt={`${hoveredTemplate.docType} ${hoveredTemplate.templateId} preview`}
className="h-full w-full object-contain"
/>
</div>
</div>
)}
</div>
</div>
)}
</div>
<div className="flex items-center gap-2">
<button
type="button"
className="flex h-8 w-8 items-center justify-center rounded-full border border-slate-200 text-slate-500"
onClick={onToggleRecording}
aria-label="Voice input"
>
🎤
</button>
<button
type="submit"
disabled={isImporting || (!prompt.trim() && !uploadedPdfFile)}
className="flex h-8 w-8 items-center justify-center rounded-full bg-slate-900 text-white disabled:opacity-40"
aria-label="Generate"
>
</button>
</div>
</div>
</>
)}
</div>
</form>
</div>
</main>
</div>
)
}
@@ -0,0 +1,75 @@
interface ImportLayoutModalProps {
isOpen: boolean
docType: string
onDocTypeChange: (value: string) => void
onClose: () => void
onFileSelected: (file: File) => void
isImporting: boolean
status: string | null
}
export function ImportLayoutModal({
isOpen,
docType,
onDocTypeChange,
onClose,
onFileSelected,
isImporting,
status,
}: ImportLayoutModalProps) {
if (!isOpen) return null
return (
<div className="fixed inset-0 bg-slate-900/70 backdrop-blur-sm z-50 flex items-center justify-center p-4">
<div className="bg-slate-900 border border-slate-700 rounded-xl shadow-2xl w-full max-w-md p-5 space-y-4">
<div className="flex items-center justify-between">
<div>
<div className="text-sm text-slate-300">Import layout from PDF</div>
<div className="text-xs text-slate-500">First 2 pages only, uses vision model</div>
</div>
<button onClick={onClose} className="text-slate-400 hover:text-white text-sm">
</button>
</div>
<div className="space-y-2">
<label className="text-xs text-slate-400">Document type</label>
<input
type="text"
value={docType}
onChange={(e) => onDocTypeChange(e.target.value)}
className="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-slate-100 text-sm"
placeholder="invoice, resume, report..."
/>
</div>
<div className="space-y-2">
<label className="text-xs text-slate-400">PDF file</label>
<input
type="file"
accept="application/pdf"
onChange={(e) => {
const file = e.target.files?.[0]
if (file) onFileSelected(file)
}}
className="w-full text-sm text-slate-300"
disabled={isImporting}
/>
</div>
{status && (
<div className="text-xs text-slate-300 bg-slate-800 border border-slate-700 rounded-lg px-3 py-2">
{status}
</div>
)}
<div className="flex items-center justify-end gap-3 text-xs text-slate-400">
<button onClick={onClose} className="px-3 py-1 rounded-lg border border-slate-700 hover:bg-slate-800">
Close
</button>
{isImporting && <span>Processing...</span>}
</div>
</div>
</div>
)
}
@@ -0,0 +1,223 @@
import { useCallback, useEffect, useMemo, useState } from 'react'
import { PDFDocument, rgb, StandardFonts } from 'pdf-lib'
import { GlobalWorkerOptions, getDocument } from 'pdfjs-dist'
GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString()
type PageTextItem = {
id: string
str: string
x: number
y: number
width: number
fontSize: number
}
type PageData = {
width: number
height: number
image: string
items: PageTextItem[]
}
interface PdfTextEditorLiteProps {
pdfUrl: string
onClose: () => void
}
export function PdfTextEditorLite({ pdfUrl, onClose }: PdfTextEditorLiteProps) {
const [pages, setPages] = useState<PageData[]>([])
const [loading, setLoading] = useState(false)
const [saving, setSaving] = useState(false)
const [error, setError] = useState<string | null>(null)
const [editedItems, setEditedItems] = useState<Record<string, string>>({})
const loadPdf = useCallback(async () => {
setLoading(true)
setError(null)
try {
const resp = await fetch(pdfUrl)
const buffer = await resp.arrayBuffer()
const pdf = await getDocument({ data: buffer }).promise
const loaded: PageData[] = []
for (let i = 1; i <= pdf.numPages; i += 1) {
const page = await pdf.getPage(i)
const viewport = page.getViewport({ scale: 1.2 })
// Render page to image
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
if (!ctx) continue
canvas.width = viewport.width
canvas.height = viewport.height
await page.render({ canvasContext: ctx, viewport }).promise
const image = canvas.toDataURL('image/png')
// Extract text items
const textContent = await page.getTextContent()
const items: PageTextItem[] = textContent.items
.map((raw, idx) => {
const it = raw as any
const transform: number[] = Array.isArray(it?.transform) ? it.transform : [1, 0, 0, 1, 0, 0]
const [a, b, , , e, f] = transform
const x = typeof e === 'number' ? e : 0
const y = typeof f === 'number' ? f : 0
const fontSize = Math.hypot(a || 0, b || 0) || it?.height || 12
const width = ((it?.width as number | undefined) || fontSize) * viewport.scale
const str = typeof it?.str === 'string' ? it.str : ''
return { id: `${i}-${idx}`, str, x, y, width, fontSize }
})
.filter((it) => it.str.length > 0)
loaded.push({
width: viewport.width,
height: viewport.height,
image,
items,
})
}
setPages(loaded)
const initialEdits: Record<string, string> = {}
loaded.forEach((p) =>
p.items.forEach((it) => {
initialEdits[it.id] = it.str
}),
)
setEditedItems(initialEdits)
} catch (e: any) {
setError(e?.message || 'Failed to load PDF')
} finally {
setLoading(false)
}
}, [pdfUrl])
useEffect(() => {
loadPdf()
}, [loadPdf])
const handleChange = (id: string, value: string) => {
setEditedItems((prev) => ({ ...prev, [id]: value }))
}
const handleDownload = useCallback(async () => {
setSaving(true)
setError(null)
try {
const doc = await PDFDocument.create()
const font = await doc.embedFont(StandardFonts.Helvetica)
for (const page of pages) {
const pdfPage = doc.addPage([page.width, page.height])
const bg = await doc.embedPng(page.image)
pdfPage.drawImage(bg, { x: 0, y: 0, width: page.width, height: page.height })
page.items.forEach((item) => {
const text = editedItems[item.id] ?? item.str
const yPdf = page.height - item.y // flip coordinate to bottom-left origin
pdfPage.drawText(text, {
x: item.x,
y: yPdf - item.fontSize,
size: item.fontSize || 12,
font,
color: rgb(0, 0, 0),
})
})
}
const bytes = await doc.save()
const arrayBuffer = new ArrayBuffer(bytes.byteLength)
new Uint8Array(arrayBuffer).set(bytes)
const blob = new Blob([arrayBuffer], { type: 'application/pdf' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = 'edited.pdf'
a.click()
URL.revokeObjectURL(url)
} catch (e: any) {
setError(e?.message || 'Failed to generate PDF')
} finally {
setSaving(false)
}
}, [editedItems, pages])
const pageCount = useMemo(() => pages.length, [pages])
return (
<div className="w-full h-full flex flex-col bg-slate-950">
<div className="flex items-center justify-between px-4 py-3 border-b border-slate-800 bg-slate-900">
<div className="flex items-center gap-3">
<h3 className="text-slate-100 font-semibold">Text Editor (lite)</h3>
<span className="text-xs text-slate-400">Pages: {pageCount || '—'}</span>
</div>
<div className="flex items-center gap-2">
<button
onClick={handleDownload}
disabled={saving || loading || pages.length === 0}
className="px-3 py-1.5 rounded bg-blue-600 text-white text-sm disabled:opacity-50"
>
{saving ? 'Saving…' : 'Download edited PDF'}
</button>
<button
onClick={onClose}
className="px-3 py-1.5 rounded bg-slate-700 text-slate-100 text-sm hover:bg-slate-600"
>
Back to preview
</button>
</div>
</div>
{error && <div className="px-4 py-2 text-sm text-amber-200 bg-amber-500/10 border border-amber-600">{error}</div>}
<div className="flex-1 overflow-y-auto p-4 space-y-8">
{loading && <div className="text-slate-300 text-sm">Loading PDF</div>}
{!loading &&
pages.map((page, pageIdx) => (
<div
key={page.image}
className="relative bg-slate-900 border border-slate-800 rounded-lg p-2 shadow"
style={{ width: page.width, minHeight: page.height }}
>
<div className="absolute inset-2">
<img
src={page.image}
alt={`Page ${pageIdx + 1}`}
className="w-full h-auto rounded border border-slate-800 shadow pointer-events-none select-none"
/>
<div className="absolute inset-0">
{page.items.map((item) => {
const yTop = page.height - item.y
return (
<div
key={item.id}
contentEditable
suppressContentEditableWarning
onInput={(e) => handleChange(item.id, (e.target as HTMLDivElement).innerText)}
className="absolute bg-transparent outline-none focus:ring-1 focus:ring-blue-400 rounded px-0.5"
style={{
left: item.x,
top: yTop - item.fontSize * 0.85,
minWidth: Math.max(item.width, 4),
fontSize: item.fontSize,
lineHeight: '1.05',
color: '#111827',
}}
>
{editedItems[item.id] ?? item.str}
</div>
)
})}
</div>
</div>
</div>
))}
{!loading && pages.length === 0 && <div className="text-slate-400 text-sm">No pages loaded.</div>}
</div>
</div>
)
}
export default PdfTextEditorLite
@@ -0,0 +1,223 @@
import { useEffect, useState } from 'react'
import { GlobalWorkerOptions, getDocument } from 'pdfjs-dist'
GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString()
type ThumbnailPage = {
id: string
dataUrl: string
width: number
height: number
}
function PdfPreviewSkeleton() {
return (
<div className="flex justify-center py-8">
<div
className="relative rounded-2xl border border-slate-300/60 bg-white shadow-2xl overflow-hidden"
style={{
width: '860px',
maxWidth: '90vw',
aspectRatio: '1 / 1.414',
}}
>
<div className="h-12 bg-slate-100 border-b border-slate-200 animate-pulse" />
<div className="p-8 space-y-4">
{[1, 2, 3, 4].map((line) => (
<div
key={`line-top-${line}`}
className="h-4 rounded-full bg-slate-200 animate-pulse"
style={{ width: `${78 - line * 10}%` }}
/>
))}
<div className="h-40 rounded-xl bg-slate-100 border border-slate-200 animate-pulse" />
{[5, 6, 7].map((line) => (
<div
key={`line-bottom-${line}`}
className="h-4 rounded-full bg-slate-200 animate-pulse"
style={{ width: `${70 - (line - 5) * 8}%` }}
/>
))}
</div>
</div>
</div>
)
}
interface PdfThumbnailViewerProps {
pdfUrl: string
isLivePreviewing?: boolean
}
export function PdfThumbnailViewer({ pdfUrl, isLivePreviewing = false }: PdfThumbnailViewerProps) {
const [pages, setPages] = useState<ThumbnailPage[]>([])
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [zoom, setZoom] = useState(1)
const [hasRenderedPage, setHasRenderedPage] = useState(false)
useEffect(() => {
if (!pdfUrl) {
setPages([])
setHasRenderedPage(false)
return
}
let cancelled = false
const controller = new AbortController()
const load = async () => {
setLoading(true)
setError(null)
try {
const response = await fetch(pdfUrl, { signal: controller.signal })
if (!response.ok) {
throw new Error('Unable to download PDF preview')
}
const buffer = await response.arrayBuffer()
const pdf = await getDocument({ data: buffer }).promise
const next: ThumbnailPage[] = []
try {
for (let pageNumber = 1; pageNumber <= pdf.numPages; pageNumber += 1) {
const page = await pdf.getPage(pageNumber)
const viewport = page.getViewport({ scale: 0.85 })
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
if (ctx) {
canvas.width = viewport.width
canvas.height = viewport.height
await page.render({ canvasContext: ctx, viewport }).promise
next.push({
id: `page-${pageNumber}`,
dataUrl: canvas.toDataURL('image/png'),
width: viewport.width,
height: viewport.height,
})
canvas.width = 0
canvas.height = 0
}
page.cleanup?.()
}
} finally {
try {
pdf.cleanup?.()
await pdf.destroy?.()
} catch {
// ignore cleanup errors
}
}
if (!cancelled) {
setPages(next)
if (next.length > 0) {
setHasRenderedPage(true)
}
}
} catch (err) {
if (!cancelled) {
const message = err instanceof Error ? err.message : 'Failed to load PDF preview'
const normalized = message.toLowerCase()
if (
isLivePreviewing &&
(normalized.includes('zero bytes') || normalized.includes('file is empty') || normalized.includes('pdf is empty'))
) {
setError(null)
} else {
setError(message)
}
}
} finally {
if (!cancelled) {
setLoading(false)
}
}
}
load()
return () => {
cancelled = true
controller.abort()
}
}, [pdfUrl, isLivePreviewing])
const shouldShowSkeleton = (loading && !hasRenderedPage && pages.length === 0) || (isLivePreviewing && pages.length === 0 && !error && !hasRenderedPage)
return (
<div className="flex h-full flex-col bg-slate-900">
{error && (
<div className="border-b border-amber-700 bg-amber-500/15 px-4 py-3 text-sm text-amber-200">
{error}
</div>
)}
<div className="relative flex-1 overflow-y-auto px-6 py-6">
<div className="sticky top-0 z-10 mb-4 flex items-center justify-end gap-3 rounded-lg border border-slate-800 bg-slate-850/80 px-3 py-2 backdrop-blur">
<span className="text-xs text-slate-300">Zoom</span>
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => setZoom((z) => Math.max(0.5, Math.round((z - 0.1) * 100) / 100))}
className="h-8 w-8 rounded border border-slate-700 bg-slate-800 text-slate-200 hover:bg-slate-750"
>
</button>
<input
type="range"
min={0.5}
max={2}
step={0.05}
value={zoom}
onChange={(e) => setZoom(Number(e.target.value))}
className="h-2 w-32 accent-blue-500"
/>
<button
type="button"
onClick={() => setZoom((z) => Math.min(2, Math.round((z + 0.1) * 100) / 100))}
className="h-8 w-8 rounded border border-slate-700 bg-slate-800 text-slate-200 hover:bg-slate-750"
>
+
</button>
<span className="w-12 text-right text-xs text-slate-300">{Math.round(zoom * 100)}%</span>
</div>
</div>
{shouldShowSkeleton && <PdfPreviewSkeleton />}
{!shouldShowSkeleton && pages.length === 0 && (
<div className="flex h-full items-center justify-center text-sm text-slate-400">Preview unavailable.</div>
)}
{!shouldShowSkeleton && pages.length > 0 && (
<div className="mx-auto flex max-w-5xl flex-col items-center gap-8">
{pages.map((page) => {
const baseScale = Math.min(920 / page.width, 1)
const displayScale = baseScale * zoom
const displayWidth = page.width * displayScale
const displayHeight = page.height * displayScale
return (
<div
key={page.id}
className="overflow-hidden rounded-lg border border-slate-800 bg-white shadow-2xl"
style={{
width: displayWidth,
height: displayHeight,
maxWidth: '100%',
}}
>
<img
src={page.dataUrl}
alt={`PDF page ${page.id}`}
className="block h-full w-full select-none object-contain"
/>
</div>
)
})}
</div>
)}
</div>
</div>
)
}
export default PdfThumbnailViewer
@@ -0,0 +1,59 @@
import React from 'react'
type ZoomControlsProps = {
value: number
onChange: (next: number) => void
min?: number
max?: number
step?: number
disabled?: boolean
className?: string
}
const clamp = (value: number, min: number, max: number) => Math.min(Math.max(value, min), max)
const formatPercent = (value: number) => `${Math.round(value * 100)}%`
const ZoomControls: React.FC<ZoomControlsProps> = ({
value,
onChange,
min = 0.2,
max = 3,
step = 0.1,
disabled = false,
className = '',
}) => {
const handleChange = (next: number) => {
if (disabled) return
const clamped = clamp(next, min, max)
onChange(Number(clamped.toFixed(2)))
}
return (
<div className={`flex items-center gap-2 ${className}`}>
<span className="hidden text-xs text-slate-400 sm:inline">Zoom</span>
<button
type="button"
onClick={() => handleChange(value - step)}
disabled={disabled}
className="rounded border border-slate-700 bg-slate-800 px-2 py-1 text-sm text-slate-200 hover:bg-slate-700 disabled:cursor-not-allowed disabled:opacity-50"
>
</button>
<div className="min-w-[56px] rounded border border-slate-700 bg-slate-800 px-2 py-1 text-center text-xs font-medium text-slate-100">
{formatPercent(value)}
</div>
<button
type="button"
onClick={() => handleChange(value + step)}
disabled={disabled}
className="rounded border border-slate-700 bg-slate-800 px-2 py-1 text-sm text-slate-200 hover:bg-slate-700 disabled:cursor-not-allowed disabled:opacity-50"
>
+
</button>
</div>
)
}
export default ZoomControls
@@ -0,0 +1,421 @@
import { PdfJsonDocument, PdfJsonFont } from './pdfTextEditorTypes';
export type FontStatus = 'perfect' | 'embedded-subset' | 'system-fallback' | 'missing' | 'unknown';
export interface FontAnalysis {
fontId: string;
baseName: string;
status: FontStatus;
embedded: boolean;
isSubset: boolean;
isStandard14: boolean;
hasWebFormat: boolean;
webFormat?: string;
subtype?: string;
encoding?: string;
warnings: string[];
suggestions: string[];
}
export interface DocumentFontAnalysis {
fonts: FontAnalysis[];
canReproducePerfectly: boolean;
hasWarnings: boolean;
summary: {
perfect: number;
embeddedSubset: number;
systemFallback: number;
missing: number;
unknown: number;
};
}
/**
* Determines if a font name indicates it's a subset font.
* Subset fonts typically have a 6-character prefix like "ABCDEE+"
*/
const isSubsetFont = (baseName: string | null | undefined): boolean => {
if (!baseName) return false;
// Check for common subset patterns: ABCDEF+FontName
return /^[A-Z]{6}\+/.test(baseName);
};
/**
* Checks if a font is one of the standard 14 PDF fonts that are guaranteed
* to be available on all PDF readers
*/
const isStandard14Font = (font: PdfJsonFont): boolean => {
if (font.standard14Name) return true;
const baseName = (font.baseName || '').toLowerCase().replace(/[-_\s]/g, '');
const standard14Patterns = [
'timesroman', 'timesbold', 'timesitalic', 'timesbolditalic',
'helvetica', 'helveticabold', 'helveticaoblique', 'helveticaboldoblique',
'courier', 'courierbold', 'courieroblique', 'courierboldoblique',
'symbol', 'zapfdingbats'
];
// Check exact matches or if the base name contains the pattern
return standard14Patterns.some(pattern => {
// Exact match
if (baseName === pattern) return true;
// Contains pattern (e.g., "ABCDEF+Helvetica" matches "helvetica")
if (baseName.includes(pattern)) return true;
return false;
});
};
/**
* Checks if a font has a fallback available on the backend.
* These fonts are embedded in the Stirling PDF backend and can be used
* for PDF export even if not in the original PDF.
*
* Based on PdfJsonFallbackFontService.java
*/
const hasBackendFallbackFont = (font: PdfJsonFont): boolean => {
const baseName = (font.baseName || '').toLowerCase().replace(/[-_\s]/g, '');
// Backend has these font families available (from PdfJsonFallbackFontService)
const backendFonts = [
// Liberation fonts (metric-compatible with MS core fonts)
'arial', 'helvetica', 'arimo',
'times', 'timesnewroman', 'tinos',
'courier', 'couriernew', 'cousine',
'liberation', 'liberationsans', 'liberationserif', 'liberationmono',
// DejaVu fonts
'dejavu', 'dejavusans', 'dejavuserif', 'dejavumono', 'dejavusansmono',
// Noto fonts
'noto', 'notosans'
];
return backendFonts.some(pattern => {
if (baseName === pattern) return true;
if (baseName.includes(pattern)) return true;
return false;
});
};
/**
* Extracts the base font name from a subset font name
* e.g., "ABCDEF+Arial" -> "Arial"
*/
const extractBaseFontName = (baseName: string | null | undefined): string | null => {
if (!baseName) return null;
const match = baseName.match(/^[A-Z]{6}\+(.+)$/);
return match ? match[1] : baseName;
};
/**
* Analyzes a single font to determine if it can be reproduced perfectly
* Takes allFonts to check if full versions of subset fonts are available
*/
export const analyzeFontReproduction = (font: PdfJsonFont, allFonts?: PdfJsonFont[]): FontAnalysis => {
const fontId = font.id || font.uid || 'unknown';
const baseName = font.baseName || 'Unknown Font';
const isSubset = isSubsetFont(font.baseName);
const isStandard14 = isStandard14Font(font);
const hasBackendFallback = hasBackendFallbackFont(font);
const embedded = font.embedded ?? false;
// Check available web formats (ordered by preference)
const webFormats = [
{ key: 'webProgram', format: font.webProgramFormat },
{ key: 'pdfProgram', format: font.pdfProgramFormat },
{ key: 'program', format: font.programFormat },
];
const availableWebFormat = webFormats.find(f => f.format);
const hasWebFormat = !!availableWebFormat;
const webFormat = availableWebFormat?.format || undefined;
const warnings: string[] = [];
const suggestions: string[] = [];
let status: FontStatus = 'unknown';
// Check if we have the full font when this is a subset
let hasFullFontVersion = false;
if (isSubset && allFonts) {
const baseFont = extractBaseFontName(font.baseName);
if (baseFont) {
// Look for a non-subset version of this font with a web format
hasFullFontVersion = allFonts.some(f => {
const otherBaseName = extractBaseFontName(f.baseName);
const isNotSubset = !isSubsetFont(f.baseName);
const hasFormat = !!(f.webProgramFormat || f.pdfProgramFormat || f.programFormat);
const sameBase = otherBaseName?.toLowerCase() === baseFont.toLowerCase();
return sameBase && isNotSubset && hasFormat && (f.embedded ?? false);
});
}
}
// Analyze font status - focusing on PDF export quality
if (isStandard14) {
// Standard 14 fonts are always available in PDF readers - perfect for export!
status = 'perfect';
suggestions.push('Standard PDF font (Times, Helvetica, or Courier). Always available in PDF readers.');
suggestions.push('Exported PDFs will render consistently across all PDF readers.');
} else if (embedded && !isSubset) {
// Perfect: Fully embedded with complete character set
status = 'perfect';
suggestions.push('Font is fully embedded. Exported PDFs will reproduce text perfectly, even with edits.');
} else if (embedded && isSubset && (hasFullFontVersion || hasBackendFallback)) {
// Subset but we have the full font or backend fallback - perfect!
status = 'perfect';
if (hasFullFontVersion) {
suggestions.push('Full font version is also available in the document. Exported PDFs can reproduce all characters.');
} else if (hasBackendFallback) {
suggestions.push('Backend has the full font available. Exported PDFs can reproduce all characters, including new text.');
}
} else if (embedded && isSubset) {
// Good, but subset: May have missing characters if user adds new text
status = 'embedded-subset';
warnings.push('This is a subset font - only specific characters are embedded in the PDF.');
warnings.push('Exported PDFs may have missing characters if you add new text with this font.');
suggestions.push('Existing text will export correctly. New characters may render as boxes (☐) or fallback glyphs.');
} else if (!embedded && hasBackendFallback) {
// Not embedded, but backend has it - perfect for export!
status = 'perfect';
suggestions.push('Backend has this font available. Exported PDFs will use the backend fallback font.');
suggestions.push('Text will export correctly with consistent appearance.');
} else if (!embedded) {
// Not embedded - must rely on system fonts (risky for export)
status = 'missing';
warnings.push('Font is not embedded in the PDF.');
warnings.push('Exported PDFs will substitute with a fallback font, which may look very different.');
suggestions.push('Consider re-embedding fonts or accepting that the exported PDF will use fallback fonts.');
} else if (embedded && !hasWebFormat) {
// Embedded but no web format available (still okay for export)
status = 'perfect';
suggestions.push('Font is embedded in the PDF. Exported PDFs will reproduce correctly.');
suggestions.push('Web preview may use a fallback font, but the final PDF export will be accurate.');
}
// Additional warnings based on font properties
if (font.subtype === 'Type0' && font.cidSystemInfo) {
const registry = font.cidSystemInfo.registry || '';
const ordering = font.cidSystemInfo.ordering || '';
if (registry.includes('Adobe') && (ordering.includes('Identity') || ordering.includes('UCS'))) {
// CID fonts with Identity encoding are common for Asian languages
if (!embedded || !hasWebFormat) {
warnings.push('This CID font may contain Asian or Unicode characters.');
}
}
}
if (font.encoding && !font.encoding.includes('WinAnsiEncoding') && !font.encoding.includes('MacRomanEncoding')) {
// Custom encodings may cause issues
if (font.encoding !== 'Identity-H' && font.encoding !== 'Identity-V') {
warnings.push(`Custom encoding detected: ${font.encoding}`);
}
}
return {
fontId,
baseName,
status,
embedded,
isSubset,
isStandard14,
hasWebFormat,
webFormat,
subtype: font.subtype || undefined,
encoding: font.encoding || undefined,
warnings,
suggestions,
};
};
/**
* Gets fonts used on a specific page
*/
export const getFontsForPage = (
document: PdfJsonDocument | null,
pageIndex: number
): PdfJsonFont[] => {
if (!document?.fonts || !document?.pages || pageIndex < 0 || pageIndex >= document.pages.length) {
return [];
}
const page = document.pages[pageIndex];
if (!page?.textElements) {
return [];
}
// Get unique font IDs used on this page
const fontIdsOnPage = new Set<string>();
page.textElements.forEach(element => {
if (element?.fontId) {
fontIdsOnPage.add(element.fontId);
}
});
// Filter fonts to only those used on this page
const allFonts = document.fonts.filter((font): font is PdfJsonFont => font !== null && font !== undefined);
const fontsOnPage = allFonts.filter(font => {
// Match by ID
if (font.id && fontIdsOnPage.has(font.id)) {
return true;
}
// Match by UID
if (font.uid && fontIdsOnPage.has(font.uid)) {
return true;
}
// Match by page-specific ID (pageNumber:id format)
if (font.pageNumber === pageIndex + 1 && font.id) {
const pageSpecificId = `${font.pageNumber}:${font.id}`;
if (fontIdsOnPage.has(pageSpecificId) || fontIdsOnPage.has(font.id)) {
return true;
}
}
return false;
});
// Deduplicate by base font name to avoid showing the same font multiple times
const uniqueFonts = new Map<string, PdfJsonFont>();
fontsOnPage.forEach(font => {
const baseName = extractBaseFontName(font.baseName) || font.baseName || font.id || 'unknown';
const key = baseName.toLowerCase();
// Keep the first occurrence, or prefer non-subset over subset
const existing = uniqueFonts.get(key);
if (!existing) {
uniqueFonts.set(key, font);
} else {
// Prefer non-subset fonts over subset fonts
const existingIsSubset = isSubsetFont(existing.baseName);
const currentIsSubset = isSubsetFont(font.baseName);
if (existingIsSubset && !currentIsSubset) {
uniqueFonts.set(key, font);
}
}
});
return Array.from(uniqueFonts.values());
};
/**
* Analyzes all fonts in a PDF document (or just fonts for a specific page)
*/
export const analyzeDocumentFonts = (
document: PdfJsonDocument | null,
pageIndex?: number
): DocumentFontAnalysis => {
if (!document?.fonts || document.fonts.length === 0) {
return {
fonts: [],
canReproducePerfectly: true,
hasWarnings: false,
summary: {
perfect: 0,
embeddedSubset: 0,
systemFallback: 0,
missing: 0,
unknown: 0,
},
};
}
const allFonts = document.fonts.filter((font): font is PdfJsonFont => font !== null && font !== undefined);
// Filter to page-specific fonts if pageIndex is provided
const fontsToAnalyze = pageIndex !== undefined
? getFontsForPage(document, pageIndex)
: allFonts;
if (fontsToAnalyze.length === 0) {
return {
fonts: [],
canReproducePerfectly: true,
hasWarnings: false,
summary: {
perfect: 0,
embeddedSubset: 0,
systemFallback: 0,
missing: 0,
unknown: 0,
},
};
}
const fontAnalyses = fontsToAnalyze.map(font => analyzeFontReproduction(font, allFonts));
// Calculate summary
const summary = {
perfect: fontAnalyses.filter(f => f.status === 'perfect').length,
embeddedSubset: fontAnalyses.filter(f => f.status === 'embedded-subset').length,
systemFallback: fontAnalyses.filter(f => f.status === 'system-fallback').length,
missing: fontAnalyses.filter(f => f.status === 'missing').length,
unknown: fontAnalyses.filter(f => f.status === 'unknown').length,
};
// Can reproduce perfectly ONLY if all fonts are truly perfect (not subsets)
const canReproducePerfectly = fontAnalyses.every(f => f.status === 'perfect');
// Has warnings if any font has issues (including subsets)
const hasWarnings = fontAnalyses.some(
f => f.warnings.length > 0 || f.status === 'missing' || f.status === 'system-fallback' || f.status === 'embedded-subset'
);
return {
fonts: fontAnalyses,
canReproducePerfectly,
hasWarnings,
summary,
};
};
/**
* Gets a human-readable description of the font status
*/
export const getFontStatusDescription = (status: FontStatus): string => {
switch (status) {
case 'perfect':
return 'Fully embedded - perfect reproduction';
case 'embedded-subset':
return 'Embedded (subset) - existing text will render correctly';
case 'system-fallback':
return 'Using system font - appearance may differ';
case 'missing':
return 'Not embedded - will use fallback font';
case 'unknown':
return 'Unknown status';
}
};
/**
* Gets a color indicator for the font status
*/
export const getFontStatusColor = (status: FontStatus): string => {
switch (status) {
case 'perfect':
return 'green';
case 'embedded-subset':
return 'blue';
case 'system-fallback':
return 'yellow';
case 'missing':
return 'red';
case 'unknown':
return 'gray';
}
};
/**
* Gets an icon indicator for the font status
*/
export const getFontStatusIcon = (status: FontStatus): string => {
switch (status) {
case 'perfect':
return '✓';
case 'embedded-subset':
return '⚠';
case 'system-fallback':
return '⚠';
case 'missing':
return '✗';
case 'unknown':
return '?';
}
};
@@ -0,0 +1,232 @@
export interface PdfJsonFontCidSystemInfo {
registry?: string | null;
ordering?: string | null;
supplement?: number | null;
}
export interface PdfJsonTextColor {
colorSpace?: string | null;
components?: number[] | null;
}
export interface PdfJsonCosValue {
type?: string | null;
value?: unknown;
items?: PdfJsonCosValue[] | null;
entries?: Record<string, PdfJsonCosValue | null> | null;
stream?: PdfJsonStream | null;
}
export interface PdfJsonFont {
id?: string;
pageNumber?: number | null;
uid?: string | null;
baseName?: string | null;
color?: string | null;
subtype?: string | null;
encoding?: string | null;
cidSystemInfo?: PdfJsonFontCidSystemInfo | null;
embedded?: boolean | null;
program?: string | null;
programFormat?: string | null;
webProgram?: string | null;
webProgramFormat?: string | null;
pdfProgram?: string | null;
pdfProgramFormat?: string | null;
toUnicode?: string | null;
standard14Name?: string | null;
fontDescriptorFlags?: number | null;
ascent?: number | null;
descent?: number | null;
capHeight?: number | null;
xHeight?: number | null;
italicAngle?: number | null;
unitsPerEm?: number | null;
cosDictionary?: PdfJsonCosValue | null;
}
export interface PdfJsonTextElement {
text?: string | null;
fontId?: string | null;
fontSize?: number | null;
fontMatrixSize?: number | null;
fontSizeInPt?: number | null;
characterSpacing?: number | null;
wordSpacing?: number | null;
spaceWidth?: number | null;
zOrder?: number | null;
horizontalScaling?: number | null;
leading?: number | null;
rise?: number | null;
renderingMode?: number | null;
x?: number | null;
y?: number | null;
width?: number | null;
height?: number | null;
textMatrix?: number[] | null;
fillColor?: PdfJsonTextColor | null;
strokeColor?: PdfJsonTextColor | null;
charCodes?: number[] | null;
fallbackUsed?: boolean | null;
}
export interface PdfJsonImageElement {
id?: string | null;
objectName?: string | null;
inlineImage?: boolean | null;
nativeWidth?: number | null;
nativeHeight?: number | null;
x?: number | null;
y?: number | null;
width?: number | null;
height?: number | null;
left?: number | null;
right?: number | null;
top?: number | null;
bottom?: number | null;
transform?: number[] | null;
zOrder?: number | null;
imageData?: string | null;
imageFormat?: string | null;
}
export interface PdfJsonStream {
dictionary?: Record<string, unknown> | null;
rawData?: string | null;
}
export interface PdfJsonPage {
pageNumber?: number | null;
width?: number | null;
height?: number | null;
rotation?: number | null;
mediaBox?: number[] | null;
cropBox?: number[] | null;
textElements?: PdfJsonTextElement[] | null;
imageElements?: PdfJsonImageElement[] | null;
resources?: unknown;
contentStreams?: PdfJsonStream[] | null;
}
export interface PdfJsonMetadata {
title?: string | null;
author?: string | null;
subject?: string | null;
keywords?: string | null;
creator?: string | null;
producer?: string | null;
creationDate?: string | null;
modificationDate?: string | null;
trapped?: string | null;
numberOfPages?: number | null;
}
export interface PdfJsonDocument {
metadata?: PdfJsonMetadata | null;
xmpMetadata?: string | null;
fonts?: PdfJsonFont[] | null;
pages?: PdfJsonPage[] | null;
lazyImages?: boolean | null;
}
export interface PdfJsonPageDimension {
pageNumber?: number | null;
width?: number | null;
height?: number | null;
rotation?: number | null;
}
export interface PdfJsonDocumentMetadata {
metadata?: PdfJsonMetadata | null;
xmpMetadata?: string | null;
fonts?: PdfJsonFont[] | null;
pageDimensions?: PdfJsonPageDimension[] | null;
formFields?: unknown[] | null;
lazyImages?: boolean | null;
}
export interface BoundingBox {
left: number;
right: number;
top: number;
bottom: number;
}
export interface TextGroup {
id: string;
pageIndex: number;
fontId?: string | null;
fontSize?: number | null;
fontMatrixSize?: number | null;
lineSpacing?: number | null;
lineElementCounts?: number[] | null;
color?: string | null;
fontWeight?: number | 'normal' | 'bold' | null;
rotation?: number | null;
anchor?: { x: number; y: number } | null;
baselineLength?: number | null;
baseline?: number | null;
elements: PdfJsonTextElement[];
originalElements: PdfJsonTextElement[];
text: string;
originalText: string;
bounds: BoundingBox;
childLineGroups?: TextGroup[] | null;
}
export const DEFAULT_PAGE_WIDTH = 612;
export const DEFAULT_PAGE_HEIGHT = 792;
export interface ConversionProgress {
percent: number;
stage: string;
message: string;
current?: number;
total?: number;
}
export interface PdfTextEditorViewData {
document: PdfJsonDocument | null;
groupsByPage: TextGroup[][];
imagesByPage: PdfJsonImageElement[][];
pagePreviews: Map<number, string>;
selectedPage: number;
dirtyPages: boolean[];
hasDocument: boolean;
hasVectorPreview: boolean;
fileName: string;
errorMessage: string | null;
isGeneratingPdf: boolean;
isConverting: boolean;
conversionProgress: ConversionProgress | null;
hasChanges: boolean;
forceSingleTextElement: boolean;
groupingMode: 'auto' | 'paragraph' | 'singleLine';
requestPagePreview: (pageIndex: number, scale: number) => void;
onSelectPage: (pageIndex: number) => void;
onGroupEdit: (pageIndex: number, groupId: string, value: string) => void;
onGroupDelete: (pageIndex: number, groupId: string) => void;
onImageTransform: (
pageIndex: number,
imageId: string,
next: {
left: number;
bottom: number;
width: number;
height: number;
transform: number[];
},
) => void;
onImageReset: (pageIndex: number, imageId: string) => void;
onReset: () => void;
onDownloadJson: () => void;
onGeneratePdf: () => void;
onGeneratePdfForNavigation: () => Promise<void>;
onSaveToWorkbench: () => Promise<void>;
isSavingToWorkbench: boolean;
onForceSingleTextElementChange: (value: boolean) => void;
onGroupingModeChange: (value: 'auto' | 'paragraph' | 'singleLine') => void;
onMergeGroups: (pageIndex: number, groupIds: string[]) => boolean;
onUngroupGroup: (pageIndex: number, groupId: string) => boolean;
onLoadFile: (file: File) => void;
}
@@ -0,0 +1,98 @@
import { useRef, useEffect, useState } from 'react'
interface AudioWaveformProps {
waveformHistory: number[][]
isActive: boolean
}
export function AudioWaveform({ waveformHistory, isActive }: AudioWaveformProps) {
const containerRef = useRef<HTMLDivElement>(null)
const [maxColumns, setMaxColumns] = useState(150)
// Measure container and calculate how many columns fit
useEffect(() => {
if (!containerRef.current) return
const updateMaxColumns = () => {
if (!containerRef.current) return
const width = containerRef.current.offsetWidth
// Each column is 2px + 2px gap = 4px effective width
const cols = Math.floor(width / 4)
setMaxColumns(Math.max(cols, 50)) // minimum 50 columns
}
updateMaxColumns()
const resizeObserver = new ResizeObserver(updateMaxColumns)
resizeObserver.observe(containerRef.current)
return () => resizeObserver.disconnect()
}, [])
if (!isActive) return null
// Use history if available, otherwise show placeholder
const columns = waveformHistory || []
// Only take exactly what fits
const visibleColumns = columns.slice(-maxColumns)
// Pad with empty columns at the start if we don't have enough history
const paddingCount = Math.max(0, maxColumns - visibleColumns.length)
return (
<div ref={containerRef} className="flex items-center h-6 w-full">
<div className="flex items-center gap-0.5 w-full">
{/* Padding columns (empty/minimal) */}
{Array(paddingCount).fill(0).map((_, i) => (
<div
key={`pad-${i}`}
className="flex flex-col items-center justify-center gap-0.5 flex-shrink-0"
style={{ width: '2px', minWidth: '2px' }}
>
{Array(10).fill(0).map((_, bandIndex) => (
<div
key={bandIndex}
className="w-full bg-blue-400/20 rounded-full"
style={{ height: '1px', minHeight: '1px' }}
/>
))}
</div>
))}
{/* Actual waveform columns */}
{visibleColumns.map((column, colIndex) => {
const numBands = 10
const step = Math.floor(column.length / numBands)
const bands = []
for (let i = 0; i < numBands; i++) {
const index = Math.min(i * step, column.length - 1)
bands.push(column[index])
}
return (
<div
key={`col-${colIndex}`}
className="flex flex-col items-center justify-center gap-0.5 flex-shrink-0"
style={{ width: '2px', minWidth: '2px' }}
>
{bands.map((level, bandIndex) => {
const height = Math.max(1, level * 20)
return (
<div
key={bandIndex}
className="w-full bg-blue-400 rounded-full"
style={{
height: `${height}px`,
minHeight: '1px',
}}
/>
)
})}
</div>
)
})}
</div>
</div>
)
}
@@ -0,0 +1,32 @@
import { ButtonHTMLAttributes } from 'react'
type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'danger' | 'accent'
type ButtonSize = 'sm' | 'md' | 'lg' | 'icon'
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: ButtonVariant
size?: ButtonSize
}
const base =
'inline-flex items-center justify-center rounded-lg font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-blue-500 focus-visible:ring-offset-slate-950 disabled:opacity-50 disabled:cursor-not-allowed'
const variantStyles: Record<ButtonVariant, string> = {
primary: 'bg-blue-600 hover:bg-blue-500 text-white',
secondary: 'bg-slate-800 hover:bg-slate-700 text-slate-100 border border-slate-700',
ghost: 'bg-transparent hover:bg-slate-900 text-slate-300 border border-transparent',
danger: 'bg-rose-600 hover:bg-rose-500 text-white',
accent: 'bg-emerald-600 hover:bg-emerald-500 text-white',
}
const sizeStyles: Record<ButtonSize, string> = {
sm: 'text-xs px-3 py-1.5',
md: 'text-sm px-4 py-2',
lg: 'text-base px-5 py-3',
icon: 'p-2',
}
export function Button({ variant = 'primary', size = 'md', className = '', ...props }: ButtonProps) {
const classes = [base, variantStyles[variant], sizeStyles[size], className].filter(Boolean).join(' ')
return <button className={classes} {...props} />
}
@@ -0,0 +1,11 @@
import { PropsWithChildren } from 'react'
interface ButtonGroupProps extends PropsWithChildren {
align?: 'start' | 'center' | 'end'
}
export function ButtonGroup({ children, align = 'start' }: ButtonGroupProps) {
const alignment =
align === 'center' ? 'justify-center' : align === 'end' ? 'justify-end' : 'justify-start'
return <div className={`flex flex-wrap gap-2 ${alignment}`}>{children}</div>
}
@@ -0,0 +1,255 @@
import { FormEvent, KeyboardEvent, MutableRefObject, useRef, useEffect } from 'react'
import { DocumentState, Message, StyleProfile } from '../../types'
import { Button } from '../ui/Button'
import { AudioWaveform } from '../ui/AudioWaveform'
interface ChatPanelProps {
onBack: () => void
styleProfile: StyleProfile | null
messages: Message[]
chatEndRef: MutableRefObject<HTMLDivElement | null>
isGenerating: boolean
prompt: string
onPromptChange: (value: string) => void
onSubmit: (event: FormEvent<HTMLFormElement>) => void
onKeyDown: (event: KeyboardEvent<HTMLTextAreaElement>) => void
skipTemplates: boolean
onSkipTemplatesChange: (value: boolean) => void
onClearSession: () => void
onToggleRecording: () => void
onCancelRecording: () => void
onAcceptRecording: () => void
isRecording: boolean
whisperStatus: string | null
waveformHistory: number[][]
currentDoc: DocumentState | null
onOpenHistory: () => void
onOpenImport: () => void
}
export function ChatPanel({
onBack,
styleProfile,
messages,
chatEndRef,
isGenerating,
prompt,
onPromptChange,
onSubmit,
onKeyDown,
skipTemplates,
onSkipTemplatesChange,
onClearSession,
onToggleRecording,
onCancelRecording,
onAcceptRecording,
isRecording,
whisperStatus,
waveformHistory,
currentDoc,
onOpenHistory,
onOpenImport,
}: ChatPanelProps) {
const textareaRef = useRef<HTMLTextAreaElement>(null)
const gradientRef = useRef<HTMLDivElement>(null)
const updateInputGradient = () => {
const gradient = gradientRef.current
if (!gradient) return
// Hide the gradient entirely to avoid lingering blur while typing.
gradient.style.opacity = '0'
}
// Auto-resize textarea and update gradient visibility
useEffect(() => {
const textarea = textareaRef.current
if (textarea) {
textarea.style.height = 'auto'
const newHeight = Math.min(textarea.scrollHeight, 200) // Max ~8 lines for the smaller panel
textarea.style.height = `${newHeight}px`
}
updateInputGradient()
}, [prompt])
return (
<div className="w-1/3 min-w-[350px] flex flex-col border-r border-slate-800 bg-slate-900 z-10">
<div className="p-4 border-b border-slate-800 flex items-center justify-between gap-3">
<Button variant="ghost" size="sm" onClick={onBack}>
Back
</Button>
<div className="flex items-center gap-2">
<Button variant="secondary" size="sm" onClick={onOpenHistory}>
History & Layouts
</Button>
<Button variant="accent" size="sm" onClick={onOpenImport}>
Import Layout (PDF)
</Button>
</div>
</div>
{styleProfile && (
<div className="px-4 py-2 border-b border-slate-800 flex items-center gap-3 text-xs text-slate-400">
<span className="px-2 py-1 rounded-full bg-slate-800 border border-slate-700">
{styleProfile.layout_preference || 'clean'}
</span>
<span className="px-2 py-1 rounded-full bg-slate-800 border border-slate-700">
{styleProfile.font_preference || 'font'}
</span>
<span className="px-2 py-1 rounded-full bg-slate-800 border border-slate-700">
{styleProfile.tone || 'tone'}
</span>
</div>
)}
<div className="flex-1 overflow-y-auto p-4 space-y-6 relative">
{messages.map((msg, idx) => (
<div key={idx} className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}>
<div
className={`max-w-[85%] rounded-2xl p-4 ${
msg.role === 'user'
? 'bg-blue-600 text-white rounded-br-none'
: 'bg-slate-800 text-slate-200 rounded-bl-none border border-slate-700'
}`}
>
<p className="whitespace-pre-wrap text-sm leading-relaxed" style={{ overflowWrap: 'anywhere', wordBreak: 'break-word' }}>{msg.text}</p>
</div>
</div>
))}
{isGenerating && (
<div className="flex justify-start">
<div className="bg-slate-800 rounded-2xl p-4 rounded-bl-none border border-slate-700">
<div className="flex space-x-2">
<div className="w-2 h-2 bg-slate-500 rounded-full animate-bounce"></div>
<div className="w-2 h-2 bg-slate-500 rounded-full animate-bounce delay-100"></div>
<div className="w-2 h-2 bg-slate-500 rounded-full animate-bounce delay-200"></div>
</div>
</div>
</div>
)}
{!messages.length && (
<div className="text-xs text-slate-500">
Ask for revisions, upload layouts, or describe the style you want. I&apos;ll keep the latest
context.
</div>
)}
<div ref={chatEndRef} />
</div>
<div className="p-4 bg-slate-900 border-t border-slate-800 space-y-3">
<div className="flex items-center justify-between text-xs text-slate-400">
<label className="flex items-center gap-2 cursor-pointer select-none">
<input
type="checkbox"
checked={skipTemplates}
onChange={(e) => onSkipTemplatesChange(e.target.checked)}
className="h-4 w-4 rounded border-slate-600 bg-slate-800 accent-blue-500"
/>
<span>Don&apos;t reuse saved layouts/templates</span>
</label>
<Button variant="secondary" size="sm" onClick={onClearSession}>
Clear
</Button>
</div>
<form onSubmit={onSubmit} className="relative">
<div className="w-full bg-slate-800 rounded-xl border border-slate-700 focus-within:ring-2 focus-within:ring-blue-500/50">
{/* Content area */}
<div className="px-3 pt-3 pb-1">
{isRecording ? (
<div className="min-h-[20px]">
<AudioWaveform waveformHistory={waveformHistory} isActive={isRecording} />
</div>
) : (
<textarea
ref={textareaRef}
value={prompt}
onChange={(e) => onPromptChange(e.target.value)}
onKeyDown={onKeyDown}
onScroll={updateInputGradient}
placeholder={currentDoc ? 'Modify the document...' : 'Describe what to build...'}
className="w-full bg-transparent text-white focus:outline-none resize-none overflow-y-auto text-sm"
style={{ maxHeight: '200px' }}
rows={1}
/>
)}
</div>
</div>
<div
ref={gradientRef}
className="pointer-events-none absolute inset-x-0 bottom-0 h-6 bg-gradient-to-t from-slate-900/90 to-transparent transition-opacity duration-200"
style={{ opacity: 0 }}
/>
<div className="mt-2 flex items-center justify-end gap-2">
{isRecording ? (
<>
<button
type="button"
onClick={onCancelRecording}
className="p-2 rounded-lg text-slate-400 hover:text-white hover:bg-slate-700 transition-colors"
title="Cancel recording"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
<button
type="button"
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
onAcceptRecording()
}}
className="p-2 rounded-full bg-blue-600 text-white hover:bg-blue-700 transition-colors"
title="Accept and transcribe"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
</button>
</>
) : (
<>
<button
type="button"
onClick={onToggleRecording}
className="p-2 rounded-lg text-slate-400 hover:text-white hover:bg-slate-700 transition-colors"
title="Start voice input"
>
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3z" />
<path d="M17 11c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08c3.39-.49 6-3.39 6-6.92h-2z" />
</svg>
</button>
<button
type="submit"
disabled={isGenerating}
className="p-2 rounded-full bg-blue-600 text-white hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
title="Send"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 10l7-7m0 0l7 7m-7-7v18" />
</svg>
</button>
</>
)}
</div>
</form>
<div className="mt-2 flex items-center gap-2 text-xs text-slate-400">
<div className="relative group px-3 py-2 rounded-full bg-slate-800 border border-slate-700 font-medium text-slate-300 cursor-not-allowed select-none">
<span className="flex items-center gap-2">
Document type (auto)
<svg className="w-3.5 h-3.5 text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 15l-7-7-7 7" />
</svg>
</span>
<span className="pointer-events-none absolute -top-9 left-1/2 -translate-x-1/2 whitespace-nowrap rounded-md bg-slate-800 px-3 py-1 text-[11px] text-slate-200 border border-slate-700 opacity-0 transition-opacity duration-150 group-hover:opacity-100">
Well pick the best document type for your prompt automatically using AI.
</span>
</div>
<div className="px-3 py-2 rounded-full bg-slate-800 border border-slate-700 font-medium text-slate-300 cursor-not-allowed select-none">
GPT 5.1
</div>
</div>
{whisperStatus && <div className="text-[11px] text-slate-400 mt-1">{whisperStatus}</div>}
</div>
</div>
)
}

Some files were not shown because too many files have changed in this diff Show More