Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

- Started the v2 cycle with a production Image category and local Image Converter.
- Added Image Resize with pixel and percentage modes, aspect-ratio preservation, optional enlargement, Original/JPEG/PNG/WebP output, and local batch ZIP saving.
- Added Image Compressor with Original/JPEG/PNG/WebP output, truthful format-specific quality behavior, unchanged dimensions, and local batch ZIP saving.
- Added per-file and aggregate compression metrics that distinguish byte savings from larger generated results.
- Added per-image output dimension/pixel checks and a 200-megapixel aggregate resize-output workload limit.
- Added JPEG, PNG, and WebP input/output, lossy quality controls for JPEG/WebP, deterministic white JPEG transparency, metadata-stripping canvas re-encoding, collision-safe Unicode names, and ZIP batch output.
- Added per-file, queue, dimension, decoded-pixel, and 200-megapixel aggregate-work protections with recoverable errors.
Expand Down
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ The v1 baseline is recorded in the [changelog](./CHANGELOG.md) and historical [v

- [Image Converter](./tools/image/converter/) — convert batches of JPEG, PNG, and WebP images locally with predictable names and ZIP output.
- [Image Resize](./tools/image/resize/) — resize image batches by pixels or percentage while preserving aspect ratio by default.
- [Image Compressor](./tools/image/compress/) — quality-compress image batches locally and compare original and result sizes.
- [Images to PDF](./tools/pdf/images-to-pdf/) — arrange JPEG, PNG, and WebP images and save them as one PDF.
- [Merge PDF](./tools/pdf/merge/) — validate, order, and combine PDF pages without rasterizing them.
- [Split PDF](./tools/pdf/split/) — extract ordered page ranges or create predictable per-page and fixed-interval archives.
Expand Down Expand Up @@ -156,6 +157,18 @@ Image Resize is available at `/tools/image/resize/` on the v2 integration branch
- Enforces the shared 50 MiB file, 100-file queue, 500 MiB queue, 16,384-pixel dimension, and 50-megapixel per-image limits, plus a 200-megapixel aggregate output-work limit.
- Keeps the queue available after validation, decode, canvas, encoding, archive, save, or cancellation errors so settings can be corrected and retried.

## Image Compressor

Image Compressor is available at `/tools/image/compress/` on the v2 integration branch.

- Accepts signature-validated JPEG, PNG, and WebP images and preserves their oriented pixel dimensions while processing sequentially in browser memory.
- Defaults to Original format. JPEG and WebP use an explicit 50–100% quality setting; PNG is re-encoded without a fake lossy-quality control, so size reduction may be limited.
- Reports original size, result size, byte difference, and signed percentage change for every file, plus aggregate batch totals. Larger results are kept and labeled as increases rather than savings.
- Preserves alpha for PNG/WebP and flattens transparent pixels onto white for JPEG. Canvas re-encoding strips EXIF and other embedded metadata.
- Uses collision-safe Unicode `_compressed` names and saves multiple outputs as `compressed_images.zip` through the same-origin JSZip dependency.
- Reuses the established input, queue, dimension, 50-megapixel per-image, and 200-megapixel aggregate decoded-work limits. Queues remain available after recoverable failures or save cancellation.
- Performs no target-size search, resizing, cropping, metadata editing, upload, analytics, telemetry, remote codec, or runtime network request.

## Images to PDF

Images to PDF is available at `/tools/pdf/images-to-pdf/`.
Expand Down
4 changes: 3 additions & 1 deletion js/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ import { es } from "./locales/es.js";
import { de } from "./locales/de.js";
import { fr } from "./locales/fr.js";
import { imageResizeLocales } from "./locales/image-resize.js";
import { imageCompressorLocales } from "./locales/image-compressor.js";

const STORAGE_KEY = "secure-tools-language";
const baseTranslations = { en, ko, ja, es, de, fr };
export const translations = Object.fromEntries(Object.entries(baseTranslations).map(([language, catalog]) => [language, {
...catalog,
metadata: { ...catalog.metadata, imageResize: imageResizeLocales[language].metadata },
metadata: { ...catalog.metadata, imageResize: imageResizeLocales[language].metadata, imageCompressor: imageCompressorLocales[language].metadata },
imageResize: imageResizeLocales[language].copy,
imageCompressor: imageCompressorLocales[language].copy,
}]));

export function resolveLanguage(value, availableLanguages = Object.keys(translations)) {
Expand Down
26 changes: 26 additions & 0 deletions js/locales/image-compressor.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions tests/i18n-quality.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ function placeholders(value) {
function testCatalogParityAndQuality() {
assert.deepEqual([...Object.keys(translations)], [...languageNames.keys()]);
const english = flatten(translations.en);
assert.equal(english.size, 597);
assert.equal(english.size, 632);

for (const [language, catalog] of Object.entries(translations)) {
const flattened = flatten(catalog);
Expand Down Expand Up @@ -77,7 +77,7 @@ function testResolutionDetectionAndPersistence() {

function testSelectorsAndDocumentTranslation() {
const pages = listFiles(root, (file) => file.endsWith(".html") && fs.readFileSync(file, "utf8").includes("data-language-select"));
assert.equal(pages.length, 17, "Every production page with the shared header must expose the language selector");
assert.equal(pages.length, 18, "Every production page with the shared header must expose the language selector");
for (const file of pages) {
const html = fs.readFileSync(file, "utf8");
const select = html.match(/<select[^>]*data-language-select[^>]*>([\s\S]*?)<\/select>/)?.[1];
Expand Down
Loading
Loading