diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index a20bdfb..ff0e8b6 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -8,15 +8,6 @@ on:
pull_request:
branches:
- main
-
-env:
- REGISTRY: ghcr.io
- IMAGE_NAME: iib0011/omni-tools
-
-permissions:
- contents: read
- packages: write
-
jobs:
test-and-build:
runs-on: ubuntu-latest
@@ -74,19 +65,11 @@ jobs:
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- - name: Log in to the Github Container registry
- uses: docker/login-action@v2
- with:
- registry: ${{ env.REGISTRY }}
- username: ${{ github.actor }}
- password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata for Docker
id: meta
uses: docker/metadata-action@v4
with:
- images: |
- ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
- iib0011/omni-tools
+ images: iib0011/omni-tools
tags: |
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
type=semver,pattern={{version}},enable=${{ startsWith(github.ref, 'refs/tags/v') }}
@@ -130,4 +113,4 @@ jobs:
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
- timeout-minutes: 20
+ timeout-minutes: 20
\ No newline at end of file
diff --git a/README.md b/README.md
index 2ac7a54..29623f9 100644
--- a/README.md
+++ b/README.md
@@ -90,10 +90,6 @@ Stay tuned as we continue to expand and improve our collection!
## Self-host/Run
-> [!TIP]
-> You can use either the DockerHub Image `iib0011/omni-tools`
-> or use the GitHub Container Registry `ghcr.io/iib0011/omni-tools`
-
### Docker
```bash
@@ -192,4 +188,4 @@ For any questions or suggestions, feel free to open an issue or contact me at:
## License
-This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
+This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
\ No newline at end of file
diff --git a/public/locales/de/image.json b/public/locales/de/image.json
index 13f8db4..e388114 100644
--- a/public/locales/de/image.json
+++ b/public/locales/de/image.json
@@ -1,6 +1,6 @@
{
"changeColors": {
- "description": "Welt",
+ "description": "Ersetze ausgewahlte Farben in einem Bild schnell und prazise durch neue Farben.",
"shortDescription": "Schnelles Austauschen der Farben in einem Bild",
"title": "Farben im Bild ändern"
},
@@ -34,7 +34,7 @@
"title": "Konvertieren Sie JPG in PNG"
},
"createTransparent": {
- "description": "Welt",
+ "description": "Mache den Hintergrund eines Bildes transparent, indem du die ausgewahlte Farbe entfernst.",
"shortDescription": "Machen Sie ein Bild schnell transparent",
"title": "Transparentes PNG erstellen"
},
@@ -61,7 +61,7 @@
"title": "QR-Code-Generator"
},
"removeBackground": {
- "description": "Welt",
+ "description": "Entferne den Hintergrund eines Bildes automatisch und behalte das Hauptmotiv transparent bei.",
"shortDescription": "Hintergründe automatisch aus Bildern entfernen",
"title": "Hintergrund aus Bild entfernen"
},
diff --git a/public/locales/de/translation.json b/public/locales/de/translation.json
index 659feb6..5292eb0 100644
--- a/public/locales/de/translation.json
+++ b/public/locales/de/translation.json
@@ -228,13 +228,10 @@
"loading": "Wird geladen... Dies kann einen Moment dauern.",
"result": "Ergebnis"
},
- "toolMultipleAudioInput": {
- "inputTitle": "Eingang {{type}}",
- "noFilesSelected": "Keine Dateien ausgewählt"
- },
- "toolMultiplePdfInput": {
- "inputTitle": "Eingang {{type}}",
- "noFilesSelected": "Keine Dateien ausgewählt"
+ "toolMultipleInput": {
+ "inputTitle": "Eingabe {{type}}",
+ "noFilesSelected": "Keine Dateien ausgewählt",
+ "deleteFile": "Datei löschen"
},
"toolOptions": {
"title": "Werkzeugoptionen"
diff --git a/public/locales/en/image.json b/public/locales/en/image.json
index a2e9414..e99cb11 100644
--- a/public/locales/en/image.json
+++ b/public/locales/en/image.json
@@ -1,8 +1,23 @@
{
"changeColors": {
- "description": "World",
+ "description": "Change specific colors in an image by selecting a source color and replacing it with a new one.",
"shortDescription": "Quickly swap colors in a image",
- "title": "Change colors in image"
+ "title": "Change colors in image",
+ "inputTitle": "Input image",
+ "resultTitle": "Result image",
+ "fromColor": {
+ "title": "From color",
+ "colorOption": "Replace a specific color",
+ "transparentOption": "Replace transparent pixels",
+ "selectorDescription": "Replace this color (from color)",
+ "similarityDescription": "Match this % of similar colors of the from color. For example, 10% white will match white and a little bit of gray."
+ },
+ "toColor": {
+ "title": "To color",
+ "colorOption": "Replace with a specific color",
+ "transparentOption": "Replace with transparent",
+ "selectorDescription": "With this color (to color)"
+ }
},
"changeOpacity": {
"description": "Easily adjust the transparency of your images. Simply upload your image, use the slider to set the desired opacity level between 0 (fully transparent) and 1 (fully opaque), and download the modified image.",
@@ -36,7 +51,7 @@
"title": "Convert JPG to PNG"
},
"createTransparent": {
- "description": "World",
+ "description": "Create a transparent version of an image by removing the chosen background color.",
"shortDescription": "Quickly make an image transparent",
"title": "Create transparent PNG"
},
@@ -84,7 +99,7 @@
"title": "QR Code Generator"
},
"removeBackground": {
- "description": "World",
+ "description": "Remove the background from an image to isolate the main subject with transparency.",
"shortDescription": "Automatically remove backgrounds from images",
"title": "Remove Background from Image"
},
@@ -119,5 +134,45 @@
"description": "Rotate an image by a specified angle.",
"shortDescription": "Rotate an image easily.",
"title": "Rotate Image"
+ },
+ "split": {
+ "title": "Split Image into Pages",
+ "shortDescription": "Split a large image into multiple pages (PDF).",
+ "description": "Split a map or large image into pages matching the selected paper format and padding. Specify pixels per printing square and square count to scale the image correctly.",
+ "inputTitle": "Input image",
+ "resultTitle": "Split PDF",
+ "loadingText": "Loading... This may take a moment.",
+ "pageParameters": {
+ "title": "Page parameters",
+ "selectPageFormat": "Select page format",
+ "padding": {
+ "label": "Padding",
+ "description": "Padding around the map on each page (in selected units)"
+ }
+ },
+ "scaleParameters": {
+ "title": "Scale parameters",
+ "pxPerSquareQuantity": {
+ "label": "Pixels per squares quantity",
+ "description": "How many pixels correspond to the specified number of squares on the original image (horizontal or vertical)"
+ },
+ "squareQuantity": {
+ "label": "Squares quantity",
+ "description": "Number of play squares across (horizontal or vertical) used for scaling"
+ },
+ "unitsPerOneSquare": {
+ "label": "Units per square",
+ "description": "Physical size of one square in the selected units"
+ }
+ },
+ "units": {
+ "title": "Units",
+ "select": "Select measurement units",
+ "options": {
+ "pt": "Points (pt)",
+ "mm": "Millimeters (mm)",
+ "in": "Inches (in)"
+ }
+ }
}
}
diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json
index b69e675..6066359 100644
--- a/public/locales/en/translation.json
+++ b/public/locales/en/translation.json
@@ -240,17 +240,10 @@
"loading": "Loading... This may take a moment.",
"result": "Result"
},
- "toolMultipleAudioInput": {
+ "toolMultipleInput": {
"inputTitle": "Input {{type}}",
- "noFilesSelected": "No files selected"
- },
- "toolMultipleImageInput": {
- "inputTitle": "Images Input",
- "noFilesSelected": "No files selected"
- },
- "toolMultiplePdfInput": {
- "inputTitle": "Input {{type}}",
- "noFilesSelected": "No files selected"
+ "noFilesSelected": "No files selected",
+ "deleteFile": "Delete file"
},
"toolOptions": {
"title": "Tool options"
diff --git a/public/locales/es/image.json b/public/locales/es/image.json
index f01d3bd..5e1f12b 100644
--- a/public/locales/es/image.json
+++ b/public/locales/es/image.json
@@ -1,6 +1,6 @@
{
"changeColors": {
- "description": "Mundo",
+ "description": "Reemplaza colores seleccionados en una imagen por nuevos colores de forma rapida y precisa.",
"shortDescription": "Intercambia colores rápidamente en una imagen",
"title": "Cambiar colores en la imagen"
},
@@ -34,7 +34,7 @@
"title": "Convertir JPG a PNG"
},
"createTransparent": {
- "description": "Mundo",
+ "description": "Haz transparente el fondo de una imagen eliminando el color seleccionado.",
"shortDescription": "Hacer que una imagen sea transparente rápidamente",
"title": "Crear PNG transparente"
},
@@ -61,7 +61,7 @@
"title": "Generador de códigos QR"
},
"removeBackground": {
- "description": "Mundo",
+ "description": "Elimina automaticamente el fondo de una imagen y deja el sujeto principal listo con transparencia.",
"shortDescription": "Eliminar automáticamente los fondos de las imágenes",
"title": "Quitar el fondo de la imagen"
},
diff --git a/public/locales/es/translation.json b/public/locales/es/translation.json
index 0bfebb0..e27ddac 100644
--- a/public/locales/es/translation.json
+++ b/public/locales/es/translation.json
@@ -228,13 +228,10 @@
"loading": "Cargando... Esto puede tardar un momento.",
"result": "Resultado"
},
- "toolMultipleAudioInput": {
- "inputTitle": "Aporte {{type}}",
- "noFilesSelected": "No hay archivos seleccionados"
- },
- "toolMultiplePdfInput": {
- "inputTitle": "Aporte {{type}}",
- "noFilesSelected": "No hay archivos seleccionados"
+ "toolMultipleInput": {
+ "inputTitle": "Entrada {{type}}",
+ "noFilesSelected": "Ningún archivo seleccionado",
+ "deleteFile": "Eliminar archivo"
},
"toolOptions": {
"title": "Opciones de herramientas"
diff --git a/public/locales/fr/image.json b/public/locales/fr/image.json
index c1713af..ba0d2c3 100644
--- a/public/locales/fr/image.json
+++ b/public/locales/fr/image.json
@@ -1,6 +1,6 @@
{
"changeColors": {
- "description": "Monde",
+ "description": "Remplace rapidement et precisement les couleurs selectionnees dans une image par de nouvelles couleurs.",
"shortDescription": "Échanger rapidement les couleurs d'une image",
"title": "Changer une couleur dans une image"
},
@@ -34,7 +34,7 @@
"title": "Convertir un JPG en PNG"
},
"createTransparent": {
- "description": "Monde",
+ "description": "Rends l'arriere-plan d'une image transparent en supprimant la couleur choisie.",
"shortDescription": "Rendre rapidement une image transparente",
"title": "Créer un PNG transparent"
},
@@ -61,7 +61,7 @@
"title": "Générateur de code QR"
},
"removeBackground": {
- "description": "Monde",
+ "description": "Supprime automatiquement l'arriere-plan d'une image et conserve le sujet principal avec transparence.",
"shortDescription": "Supprimer automatiquement les arrière-plans des images",
"title": "Supprimer l'arrière-plan de l'image"
},
diff --git a/public/locales/fr/translation.json b/public/locales/fr/translation.json
index 2a9f2b9..7575135 100644
--- a/public/locales/fr/translation.json
+++ b/public/locales/fr/translation.json
@@ -228,13 +228,10 @@
"loading": "Chargement... Cela peut prendre un moment.",
"result": "Résultat"
},
- "toolMultipleAudioInput": {
- "inputTitle": "Saisir {{type}}",
- "noFilesSelected": "Aucun fichier sélectionné"
- },
- "toolMultiplePdfInput": {
- "inputTitle": "Saisir {{type}}",
- "noFilesSelected": "Aucun fichier sélectionné"
+ "toolMultipleInput": {
+ "inputTitle": "Entrée {{type}}",
+ "noFilesSelected": "Aucun fichier sélectionné",
+ "deleteFile": "Supprimer le fichier"
},
"toolOptions": {
"title": "Options d'outils"
diff --git a/public/locales/hi/image.json b/public/locales/hi/image.json
index c6f7616..2a1b5d9 100644
--- a/public/locales/hi/image.json
+++ b/public/locales/hi/image.json
@@ -1,6 +1,6 @@
{
"changeColors": {
- "description": "दुनिया",
+ "description": "चित्र में चुने गए रंगों को जल्दी और सही तरीके से नए रंगों से बदलें।",
"shortDescription": "किसी छवि में रंगों को शीघ्रता से बदलें",
"title": "छवि में रंग बदलें"
},
@@ -59,7 +59,7 @@
"title": "JPG को PNG में बदलें"
},
"createTransparent": {
- "description": "दुनिया",
+ "description": "चुने गए रंग को हटाकर चित्र की पृष्ठभूमि को पारदर्शी बनाएं।",
"shortDescription": "किसी छवि को शीघ्रता से पारदर्शी बनाएँ",
"title": "पारदर्शी PNG बनाएँ"
},
@@ -133,7 +133,7 @@
"title": "क्यूआर कोड जनरेटर"
},
"removeBackground": {
- "description": "दुनिया",
+ "description": "चित्र की पृष्ठभूमि को अपने आप हटाएं और मुख्य विषय को पारदर्शी रूप में रखें।",
"shortDescription": "छवियों से पृष्ठभूमि स्वचालित रूप से हटाएँ",
"title": "छवि से पृष्ठभूमि हटाएँ"
},
diff --git a/public/locales/hi/translation.json b/public/locales/hi/translation.json
index b54243c..d83c086 100644
--- a/public/locales/hi/translation.json
+++ b/public/locales/hi/translation.json
@@ -228,13 +228,10 @@
"loading": "लोड हो रहा है... इसमें कुछ समय लग सकता है।",
"result": "परिणाम"
},
- "toolMultipleAudioInput": {
- "inputTitle": "इनपुट {{type}}",
- "noFilesSelected": "कोई फ़ाइल चयनित नहीं"
- },
- "toolMultiplePdfInput": {
- "inputTitle": "इनपुट {{type}}",
- "noFilesSelected": "कोई फ़ाइल चयनित नहीं"
+ "toolMultipleInput": {
+ "inputTitle": "{{type}} इनपुट करें",
+ "noFilesSelected": "कोई फ़ाइल नहीं चुनी गई",
+ "deleteFile": "फ़ाइल हटाएं"
},
"toolOptions": {
"title": "टूल विकल्प"
diff --git a/public/locales/ja/image.json b/public/locales/ja/image.json
index 547da21..1a01f8c 100644
--- a/public/locales/ja/image.json
+++ b/public/locales/ja/image.json
@@ -1,6 +1,6 @@
{
"changeColors": {
- "description": "世界",
+ "description": "画像内の選択した色をすばやく正確に新しい色へ置き換えます。",
"shortDescription": "画像内の色を素早く入れ替える",
"title": "画像の色を変更する"
},
@@ -34,7 +34,7 @@
"title": "JPGをPNGに変換する"
},
"createTransparent": {
- "description": "世界",
+ "description": "選択した色を削除して、画像の背景を透明にします。",
"shortDescription": "画像を素早く透明にする",
"title": "透明なPNGを作成する"
},
@@ -61,7 +61,7 @@
"title": "QRコードジェネレーター"
},
"removeBackground": {
- "description": "世界",
+ "description": "画像の背景を自動で削除し、主要な被写体を透過状態で残します。",
"shortDescription": "画像から背景を自動的に削除する",
"title": "画像から背景を削除する"
},
diff --git a/public/locales/ja/translation.json b/public/locales/ja/translation.json
index 41cc9eb..89ea221 100644
--- a/public/locales/ja/translation.json
+++ b/public/locales/ja/translation.json
@@ -228,13 +228,10 @@
"loading": "読み込み中... 少々時間がかかる場合があります。",
"result": "結果"
},
- "toolMultipleAudioInput": {
- "inputTitle": "入力 {{type}}",
- "noFilesSelected": "ファイルが選択されていません"
- },
- "toolMultiplePdfInput": {
- "inputTitle": "入力 {{type}}",
- "noFilesSelected": "ファイルが選択されていません"
+ "toolMultipleInput": {
+ "inputTitle": "{{type}}を入力",
+ "noFilesSelected": "ファイルが選択されていません",
+ "deleteFile": "ファイルを削除"
},
"toolOptions": {
"title": "ツールオプション"
diff --git a/public/locales/nl/image.json b/public/locales/nl/image.json
index 25a9e3e..eaf5b3a 100644
--- a/public/locales/nl/image.json
+++ b/public/locales/nl/image.json
@@ -1,6 +1,6 @@
{
"changeColors": {
- "description": "Wereld",
+ "description": "Vervang geselecteerde kleuren in een afbeelding snel en nauwkeurig door nieuwe kleuren.",
"shortDescription": "Snel kleuren in een afbeelding verwisselen",
"title": "Kleuren in afbeelding wijzigen"
},
@@ -34,7 +34,7 @@
"title": "JPG naar PNG converteren"
},
"createTransparent": {
- "description": "Wereld",
+ "description": "Maak de achtergrond van een afbeelding transparant door de gekozen kleur te verwijderen.",
"shortDescription": "Snel een afbeelding transparant maken",
"title": "Transparante PNG maken"
},
@@ -61,7 +61,7 @@
"title": "QR-codegenerator"
},
"removeBackground": {
- "description": "Wereld",
+ "description": "Verwijder automatisch de achtergrond van een afbeelding en behoud het hoofdonderwerp met transparantie.",
"shortDescription": "Achtergronden automatisch uit afbeeldingen verwijderen",
"title": "Achtergrond uit afbeelding verwijderen"
},
diff --git a/public/locales/nl/translation.json b/public/locales/nl/translation.json
index a042400..12e11dc 100644
--- a/public/locales/nl/translation.json
+++ b/public/locales/nl/translation.json
@@ -228,13 +228,10 @@
"loading": "Bezig met laden... Dit kan even duren.",
"result": "Resultaat"
},
- "toolMultipleAudioInput": {
+ "toolMultipleInput": {
"inputTitle": "Invoer {{type}}",
- "noFilesSelected": "Geen bestanden geselecteerd"
- },
- "toolMultiplePdfInput": {
- "inputTitle": "Invoer {{type}}",
- "noFilesSelected": "Geen bestanden geselecteerd"
+ "noFilesSelected": "Geen bestanden geselecteerd",
+ "deleteFile": "Bestand verwijderen"
},
"toolOptions": {
"title": "Gereedschapsopties"
diff --git a/public/locales/pt/image.json b/public/locales/pt/image.json
index adbc7af..d53f8af 100644
--- a/public/locales/pt/image.json
+++ b/public/locales/pt/image.json
@@ -1,6 +1,6 @@
{
"changeColors": {
- "description": "Mundo",
+ "description": "Substitua cores selecionadas em uma imagem por novas cores de forma rapida e precisa.",
"shortDescription": "Troque cores rapidamente em uma imagem",
"title": "Alterar cores na imagem"
},
@@ -34,7 +34,7 @@
"title": "Converter JPG para PNG"
},
"createTransparent": {
- "description": "Mundo",
+ "description": "Torne o fundo de uma imagem transparente removendo a cor escolhida.",
"shortDescription": "Tornar uma imagem transparente rapidamente",
"title": "Criar PNG transparente"
},
@@ -61,7 +61,7 @@
"title": "Gerador de código QR"
},
"removeBackground": {
- "description": "Mundo",
+ "description": "Remova automaticamente o fundo de uma imagem e mantenha o assunto principal com transparencia.",
"shortDescription": "Remover fundos de imagens automaticamente",
"title": "Remover fundo da imagem"
},
diff --git a/public/locales/pt/translation.json b/public/locales/pt/translation.json
index 35a1807..74cb324 100644
--- a/public/locales/pt/translation.json
+++ b/public/locales/pt/translation.json
@@ -228,13 +228,10 @@
"loading": "Carregando... Isso pode demorar um momento.",
"result": "Resultado"
},
- "toolMultipleAudioInput": {
+ "toolMultipleInput": {
"inputTitle": "Entrada {{type}}",
- "noFilesSelected": "Nenhum arquivo selecionado"
- },
- "toolMultiplePdfInput": {
- "inputTitle": "Entrada {{type}}",
- "noFilesSelected": "Nenhum arquivo selecionado"
+ "noFilesSelected": "Nenhum ficheiro selecionado",
+ "deleteFile": "Eliminar ficheiro"
},
"toolOptions": {
"title": "Opções de ferramentas"
diff --git a/public/locales/ru/image.json b/public/locales/ru/image.json
index 689968f..8290de1 100644
--- a/public/locales/ru/image.json
+++ b/public/locales/ru/image.json
@@ -1,6 +1,6 @@
{
"changeColors": {
- "description": "Мир",
+ "description": "Быстро и точно заменяйте выбранные цвета на изображении новыми цветами.",
"shortDescription": "Быстро поменяйте цвета на изображении",
"title": "Изменить цвета на изображении"
},
@@ -34,7 +34,7 @@
"title": "Конвертировать JPG в PNG"
},
"createTransparent": {
- "description": "Мир",
+ "description": "Сделайте фон изображения прозрачным, удалив выбранный цвет.",
"shortDescription": "Быстро сделать изображение прозрачным",
"title": "Создать прозрачный PNG"
},
@@ -61,7 +61,7 @@
"title": "Генератор QR-кода"
},
"removeBackground": {
- "description": "Мир",
+ "description": "Автоматически удалите фон изображения и сохраните основной объект на прозрачном фоне.",
"shortDescription": "Автоматически удалять фоны с изображений",
"title": "Удалить фон из изображения"
},
@@ -96,5 +96,45 @@
"description": "Повернуть изображение на указанный угол.",
"shortDescription": "Легко поворачивайте изображение.",
"title": "Повернуть изображение"
+ },
+ "split": {
+ "title": "Разбить изображение по страницам",
+ "shortDescription": "Разбивает большое изображение на несколько страниц (PDF).",
+ "description": "Разбивает карту или большое изображение на страницы выбранного формата с учётом полей. Укажите количество пикселей на заданное число клеток и их количество для корректного масштабирования.",
+ "inputTitle": "Входное изображение",
+ "resultTitle": "PDF с разрезанными страницами",
+ "loadingText": "Загрузка... Это может занять некоторое время.",
+ "pageParameters": {
+ "title": "Параметры страницы",
+ "selectPageFormat": "Выберите формат страницы",
+ "padding": {
+ "label": "Отступ",
+ "description": "Отступ вокруг карты на каждой странице (в выбранных единицах)"
+ }
+ },
+ "scaleParameters": {
+ "title": "Параметры масштаба",
+ "pxPerSquareQuantity": {
+ "label": "Пикселей на количество клеток",
+ "description": "Сколько пикселей соответствует указанному количеству клеток на исходном изображении (по горизонтали или вертикали)"
+ },
+ "squareQuantity": {
+ "label": "Количество клеток",
+ "description": "Число игровых клеток по горизонтали или вертикали, используемое для масштабирования"
+ },
+ "unitsPerOneSquare": {
+ "label": "Размер клетки",
+ "description": "Физический размер одной клетки в выбранных единицах"
+ }
+ },
+ "units": {
+ "title": "Единицы измерения",
+ "select": "Выберите единицы измерения",
+ "options": {
+ "pt": "Пункты (pt)",
+ "mm": "Миллиметры (мм)",
+ "in": "Дюймы (in)"
+ }
+ }
}
}
diff --git a/public/locales/ru/translation.json b/public/locales/ru/translation.json
index f0bfca4..7b4cf97 100644
--- a/public/locales/ru/translation.json
+++ b/public/locales/ru/translation.json
@@ -228,13 +228,10 @@
"loading": "Загрузка... Это может занять некоторое время.",
"result": "Результат"
},
- "toolMultipleAudioInput": {
- "inputTitle": "Вход {{type}}",
- "noFilesSelected": "Файлы не выбраны."
- },
- "toolMultiplePdfInput": {
- "inputTitle": "Вход {{type}}",
- "noFilesSelected": "Файлы не выбраны."
+ "toolMultipleInput": {
+ "inputTitle": "Ввод {{type}}",
+ "noFilesSelected": "Файлы не выбраны",
+ "deleteFile": "Удалить файл"
},
"toolOptions": {
"title": "Параметры инструмента"
diff --git a/public/locales/zh/image.json b/public/locales/zh/image.json
index 7fa45ab..dc1887c 100644
--- a/public/locales/zh/image.json
+++ b/public/locales/zh/image.json
@@ -1,6 +1,6 @@
{
"changeColors": {
- "description": "世界",
+ "description": "快速准确地将图像中的选定颜色替换为新颜色。",
"shortDescription": "快速交换图像中的颜色",
"title": "改变图像的颜色"
},
@@ -34,7 +34,7 @@
"title": "JPG 转换为 PNG"
},
"createTransparent": {
- "description": "世界",
+ "description": "通过移除所选颜色,使图像背景变为透明。",
"shortDescription": "快速使图像透明",
"title": "创建透明 PNG"
},
@@ -61,7 +61,7 @@
"title": "二维码生成器"
},
"removeBackground": {
- "description": "世界",
+ "description": "自动移除图像背景,并保留主体为透明背景。",
"shortDescription": "自动删除图像的背景",
"title": "从图像中删除背景"
},
diff --git a/public/locales/zh/translation.json b/public/locales/zh/translation.json
index 25646db..8f89155 100644
--- a/public/locales/zh/translation.json
+++ b/public/locales/zh/translation.json
@@ -236,13 +236,10 @@
"loading": "正在加载...这可能需要一点时间。",
"result": "结果"
},
- "toolMultipleAudioInput": {
+ "toolMultipleInput": {
"inputTitle": "输入 {{type}}",
- "noFilesSelected": "未选择任何文件"
- },
- "toolMultiplePdfInput": {
- "inputTitle": "输入 {{type}}",
- "noFilesSelected": "未选择任何文件"
+ "noFilesSelected": "未选择任何文件",
+ "deleteFile": "删除文件"
},
"toolOptions": {
"title": "工具选项"
diff --git a/src/components/input/ToolMultipleAudioInput.tsx b/src/components/input/ToolMultipleAudioInput.tsx
index f6729cc..e1682e7 100644
--- a/src/components/input/ToolMultipleAudioInput.tsx
+++ b/src/components/input/ToolMultipleAudioInput.tsx
@@ -1,11 +1,10 @@
-import { ReactNode, useContext, useEffect, useRef, useState } from 'react';
-import { Box, useTheme } from '@mui/material';
+import { useRef } from 'react';
+import { Box, IconButton, Tooltip, useTheme } from '@mui/material';
import Typography from '@mui/material/Typography';
import InputHeader from '../InputHeader';
import InputFooter from './InputFooter';
-import { CustomSnackBarContext } from '../../contexts/CustomSnackBarContext';
-import { isArray } from 'lodash';
import MusicNoteIcon from '@mui/icons-material/MusicNote';
+import ClearIcon from '@mui/icons-material/Clear';
import { useTranslation } from 'react-i18next';
interface MultiAudioInputComponentProps {
@@ -31,7 +30,6 @@ export default function ToolMultipleAudioInput({
const { t } = useTranslation();
const theme = useTheme();
const fileInputRef = useRef(null);
- const { showSnackBar } = useContext(CustomSnackBarContext);
const handleFileChange = (event: React.ChangeEvent) => {
const files = event.target.files;
@@ -99,7 +97,7 @@ export default function ToolMultipleAudioInput({
-
-
-
- {fileNameTruncate(file.file.name)}
-
-
- {
- const updatedFiles = value.filter((_, i) => i !== index);
- onChange(updatedFiles);
- }}
- >
- ✖
-
+
+
+
+
+ {fileNameTruncate(file.file.name)}
+
+
+
+
+ {
+ const updatedFiles = value.filter((_, i) => i !== index);
+ onChange(updatedFiles);
+ }}
+ >
+
+
+
))
) : (
- {t('toolMultipleAudioInput.noFilesSelected')}
+ {t('toolMultipleInput.noFilesSelected')}
)}
diff --git a/src/components/input/ToolMultipleImageInput.tsx b/src/components/input/ToolMultipleImageInput.tsx
index 13094dc..6804caa 100644
--- a/src/components/input/ToolMultipleImageInput.tsx
+++ b/src/components/input/ToolMultipleImageInput.tsx
@@ -104,7 +104,7 @@ export default function ToolMultiImageInput({
- {t('toolMultipleImageInput.noFilesSelected')}
+ {t('toolMultipleInput.noFilesSelected')}
)}
diff --git a/src/components/input/ToolMultiplePdfInput.tsx b/src/components/input/ToolMultiplePdfInput.tsx
index 67f874f..304d28f 100644
--- a/src/components/input/ToolMultiplePdfInput.tsx
+++ b/src/components/input/ToolMultiplePdfInput.tsx
@@ -1,10 +1,9 @@
-import { ReactNode, useContext, useEffect, useRef, useState } from 'react';
-import { Box, useTheme } from '@mui/material';
+import { useRef } from 'react';
+import { Box, IconButton, Tooltip, useTheme } from '@mui/material';
import Typography from '@mui/material/Typography';
import InputHeader from '../InputHeader';
import InputFooter from './InputFooter';
-import { CustomSnackBarContext } from '../../contexts/CustomSnackBarContext';
-import { isArray } from 'lodash';
+import ClearIcon from '@mui/icons-material/Clear';
import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf';
import { useTranslation } from 'react-i18next';
@@ -31,7 +30,6 @@ export default function ToolMultiFileInput({
const { t } = useTranslation();
const theme = useTheme();
const fileInputRef = useRef(null);
- const { showSnackBar } = useContext(CustomSnackBarContext);
const handleFileChange = (event: React.ChangeEvent) => {
const files = event.target.files;
@@ -65,7 +63,6 @@ export default function ToolMultiFileInput({
};
const reorderList = (sourceIndex: number, destinationIndex: number) => {
- console.log(sourceIndex, destinationIndex);
if (destinationIndex === sourceIndex) {
return;
}
@@ -100,7 +97,7 @@ export default function ToolMultiFileInput({
-
-
-
- {fileNameTruncate(file.file.name)}
-
-
- {
- const updatedFiles = value.filter((_, i) => i !== index);
- onChange(updatedFiles);
- }}
- >
- ✖
-
+
+
+
+
+ {fileNameTruncate(file.file.name)}
+
+
+
+
+ {
+ const updatedFiles = value.filter((_, i) => i !== index);
+ onChange(updatedFiles);
+ }}
+ >
+
+
+
))
) : (
- {t('toolMultiplePdfInput.noFilesSelected')}
+ {t('toolMultipleInput.noFilesSelected')}
)}
diff --git a/src/components/input/ToolMultipleVideoInput.tsx b/src/components/input/ToolMultipleVideoInput.tsx
index 9f42b8a..d861df1 100644
--- a/src/components/input/ToolMultipleVideoInput.tsx
+++ b/src/components/input/ToolMultipleVideoInput.tsx
@@ -1,11 +1,11 @@
-import { ReactNode, useContext, useEffect, useRef, useState } from 'react';
-import { Box, useTheme } from '@mui/material';
+import { useRef } from 'react';
+import { Box, IconButton, Tooltip } from '@mui/material';
import Typography from '@mui/material/Typography';
import InputHeader from '../InputHeader';
import InputFooter from './InputFooter';
-import { CustomSnackBarContext } from '../../contexts/CustomSnackBarContext';
-import { isArray } from 'lodash';
import VideoFileIcon from '@mui/icons-material/VideoFile';
+import ClearIcon from '@mui/icons-material/Clear';
+import { useTranslation } from 'react-i18next';
interface MultiVideoInputComponentProps {
accept: string[];
@@ -27,13 +27,11 @@ export default function ToolMultipleVideoInput({
title,
type
}: MultiVideoInputComponentProps) {
- console.log('ToolMultipleVideoInput rendering with value:', value);
-
+ const { t } = useTranslation();
const fileInputRef = useRef(null);
const handleFileChange = (event: React.ChangeEvent) => {
const files = event.target.files;
- console.log('File change event:', files);
if (files)
onChange([
...value,
@@ -42,12 +40,10 @@ export default function ToolMultipleVideoInput({
};
const handleImportClick = () => {
- console.log('Import clicked');
fileInputRef.current?.click();
};
function handleClear() {
- console.log('Clear clicked');
onChange([]);
}
@@ -98,7 +94,12 @@ export default function ToolMultipleVideoInput({
return (
-
-
-
- {fileNameTruncate(file.file.name)}
-
-
- {
- const updatedFiles = value.filter((_, i) => i !== index);
- onChange(updatedFiles);
- }}
- >
- ✖
-
+
+
+
+
+ {fileNameTruncate(file.file.name)}
+
+
+
+
+ {
+ const updatedFiles = value.filter((_, i) => i !== index);
+ onChange(updatedFiles);
+ }}
+ >
+
+
+
))
) : (
- No files selected
+ {t('toolMultipleInput.noFilesSelected')}
)}
diff --git a/src/pages/tools/image/generic/change-colors/index.tsx b/src/pages/tools/image/generic/change-colors/index.tsx
index 1eb5745..765fe89 100644
--- a/src/pages/tools/image/generic/change-colors/index.tsx
+++ b/src/pages/tools/image/generic/change-colors/index.tsx
@@ -4,73 +4,129 @@ import * as Yup from 'yup';
import ToolFileResult from '@components/result/ToolFileResult';
import { GetGroupsType } from '@components/options/ToolOptions';
import ColorSelector from '@components/options/ColorSelector';
+import { updateNumberField } from '@utils/string';
import Color from 'color';
import TextFieldWithDesc from 'components/options/TextFieldWithDesc';
+import SimpleRadio from '@components/options/SimpleRadio';
import ToolContent from '@components/ToolContent';
import { ToolComponentProps } from '@tools/defineTool';
import ToolImageInput from '@components/input/ToolImageInput';
import { processImage } from './service';
+import { InitialValuesType } from './types';
+import { useTranslation } from 'react-i18next';
-const initialValues = {
- fromColor: 'white',
- toColor: 'black',
- similarity: '10'
+const initialValues: InitialValuesType = {
+ fromColor: '#ffffffff',
+ toColor: '#000000ff',
+ similarity: 10
};
+
const validationSchema = Yup.object({
- // splitSeparator: Yup.string().required('The separator is required')
+ similarity: Yup.number()
+ .min(0, 'Similarity must be between 0 and 100')
+ .max(100, 'Similarity must be between 0 and 100')
+ .required()
});
+
export default function ChangeColorsInImage({ title }: ToolComponentProps) {
+ const { t } = useTranslation('image');
const [input, setInput] = useState(null);
const [result, setResult] = useState(null);
+ const [isProcessing, setIsProcessing] = useState(false);
- const compute = (optionsValues: typeof initialValues, input: any) => {
+ const compute = async (
+ optionsValues: InitialValuesType,
+ input: File | null
+ ) => {
if (!input) return;
const { fromColor, toColor, similarity } = optionsValues;
- let fromRgb: [number, number, number];
- let toRgb: [number, number, number];
+ let fromRgb: [number, number, number, number];
+ let toRgb: [number, number, number, number];
try {
- //@ts-ignore
- fromRgb = Color(fromColor).rgb().array();
- //@ts-ignore
- toRgb = Color(toColor).rgb().array();
+ fromRgb = [
+ ...Color(fromColor).rgb().array(),
+ Color(fromColor).alpha() * 255
+ ] as [number, number, number, number];
+ toRgb = [
+ ...Color(toColor).rgb().array(),
+ Color(toColor).alpha() * 255
+ ] as [number, number, number, number];
} catch (err) {
return;
}
- processImage(input, fromRgb, toRgb, Number(similarity), setResult);
+ setIsProcessing(true);
+ try {
+ const image = await processImage(input, fromRgb, toRgb, similarity);
+ setResult(image);
+ } finally {
+ setIsProcessing(false);
+ }
};
- const getGroups: GetGroupsType = ({
+ const getGroups: GetGroupsType = ({
values,
updateField
}) => [
{
- title: 'From color and to color',
+ title: t('changeColors.fromColor.title'),
component: (
- updateField('fromColor', val)}
- description={'Replace this color (from color)'}
- inputProps={{ 'data-testid': 'from-color-input' }}
+ updateField('fromColor', '#00000000')}
/>
- updateField('toColor', val)}
- description={'With this color (to color)'}
- inputProps={{ 'data-testid': 'to-color-input' }}
+ updateField('fromColor', '#ffffffff')}
/>
+ {values.fromColor !== '#00000000' && (
+ updateField('fromColor', val)}
+ description={t('changeColors.fromColor.selectorDescription')}
+ inputProps={{ 'data-testid': 'from-color-input' }}
+ />
+ )}
updateField('similarity', val)}
- description={
- 'Match this % of similar colors of the from color. For example, 10% white will match white and a little bit of gray.'
+ onOwnChange={(val) =>
+ updateNumberField(val, 'similarity', updateField)
}
+ description={t('changeColors.fromColor.similarityDescription')}
/>
)
+ },
+ {
+ title: t('changeColors.toColor.title'),
+ component: (
+
+ updateField('toColor', '#00000000')}
+ />
+ updateField('toColor', '#000000ff')}
+ />
+ {values.toColor !== '#00000000' && (
+ updateField('toColor', val)}
+ description={t('changeColors.toColor.selectorDescription')}
+ inputProps={{ 'data-testid': 'to-color-input' }}
+ />
+ )}
+
+ )
}
];
+
return (
+ }
+ resultComponent={
+
}
- resultComponent={}
/>
);
}
diff --git a/src/pages/tools/image/generic/change-colors/service.ts b/src/pages/tools/image/generic/change-colors/service.ts
index a35ed1d..5b69390 100644
--- a/src/pages/tools/image/generic/change-colors/service.ts
+++ b/src/pages/tools/image/generic/change-colors/service.ts
@@ -2,168 +2,181 @@ import { areColorsSimilar } from '@utils/color';
export const processImage = async (
file: File,
- fromColor: [number, number, number],
- toColor: [number, number, number],
- similarity: number,
- setResult: (result: File | null) => void
-): Promise => {
+ fromColor: [number, number, number, number],
+ toColor: [number, number, number, number],
+ similarity: number
+): Promise => {
if (file.type === 'image/svg+xml') {
- const reader = new FileReader();
- reader.onload = (e) => {
- if (!e.target?.result) return;
+ return new Promise((resolve) => {
+ const reader = new FileReader();
+ reader.onload = (e) => {
+ if (!e.target?.result) return;
- let svgContent = e.target.result as string;
- const toColorHex = rgbToHex(toColor[0], toColor[1], toColor[2]);
+ let svgContent = e.target.result as string;
+ const toColorHex = rgbToHex(
+ toColor[0],
+ toColor[1],
+ toColor[2],
+ toColor[3]
+ );
- // Replace hex colors with various formats (#fff, #ffffff)
- const hexRegexShort = new RegExp(`#[0-9a-f]{3}\\b`, 'gi');
- const hexRegexLong = new RegExp(`#[0-9a-f]{6}\\b`, 'gi');
+ const hexRegexShort = new RegExp(`#[0-9a-f]{3}\\b`, 'gi');
+ const hexRegexLong = new RegExp(`#[0-9a-f]{6}\\b`, 'gi');
+ const hexRegexAlpha = new RegExp(`#[0-9a-f]{8}\\b`, 'gi');
- svgContent = svgContent.replace(hexRegexShort, (match) => {
- // Expand short hex to full form for comparison
- const expanded =
- '#' + match[1] + match[1] + match[2] + match[2] + match[3] + match[3];
- const matchRgb = hexToRgb(expanded);
- if (matchRgb && areColorsSimilar(matchRgb, fromColor, similarity)) {
- return toColorHex;
- }
- return match;
- });
+ svgContent = svgContent.replace(hexRegexShort, (match) => {
+ const expanded =
+ '#' +
+ match[1] +
+ match[1] +
+ match[2] +
+ match[2] +
+ match[3] +
+ match[3];
+ const matchRgb = hexToRgb(expanded);
+ if (matchRgb && areColorsSimilar(matchRgb, fromColor, similarity))
+ return toColorHex;
+ return match;
+ });
- svgContent = svgContent.replace(hexRegexLong, (match) => {
- const matchRgb = hexToRgb(match);
- if (matchRgb && areColorsSimilar(matchRgb, fromColor, similarity)) {
- return toColorHex;
- }
- return match;
- });
+ svgContent = svgContent.replace(hexRegexLong, (match) => {
+ const matchRgb = hexToRgb(match);
+ if (matchRgb && areColorsSimilar(matchRgb, fromColor, similarity))
+ return toColorHex;
+ return match;
+ });
- // Replace RGB colors
- const rgbRegex = new RegExp(
- `rgb\\(\\s*(\\d+)\\s*,\\s*(\\d+)\\s*,\\s*(\\d+)\\s*\\)`,
- 'gi'
- );
- svgContent = svgContent.replace(rgbRegex, (match, r, g, b) => {
- const matchRgb: [number, number, number] = [
- parseInt(r),
- parseInt(g),
- parseInt(b)
- ];
- if (areColorsSimilar(matchRgb, fromColor, similarity)) {
- return `rgb(${toColor[0]}, ${toColor[1]}, ${toColor[2]})`;
- }
- return match;
- });
+ svgContent = svgContent.replace(hexRegexAlpha, (match) => {
+ const matchRgba = hexToRgba(match);
+ if (matchRgba && areColorsSimilar(matchRgba, fromColor, similarity))
+ return toColorHex;
+ return match;
+ });
- // Replace RGBA colors (preserving alpha)
- const rgbaRegex = new RegExp(
- `rgba\\(\\s*(\\d+)\\s*,\\s*(\\d+)\\s*,\\s*(\\d+)\\s*,\\s*([\\d.]+)\\s*\\)`,
- 'gi'
- );
- svgContent = svgContent.replace(rgbaRegex, (match, r, g, b, a) => {
- const matchRgb: [number, number, number] = [
- parseInt(r),
- parseInt(g),
- parseInt(b)
- ];
- if (areColorsSimilar(matchRgb, fromColor, similarity)) {
- return `rgba(${toColor[0]}, ${toColor[1]}, ${toColor[2]}, ${a})`;
- }
- return match;
- });
+ const rgbRegex = new RegExp(
+ `rgb\\(\\s*(\\d+)\\s*,\\s*(\\d+)\\s*,\\s*(\\d+)\\s*\\)`,
+ 'gi'
+ );
+ svgContent = svgContent.replace(rgbRegex, (match, r, g, b) => {
+ const matchRgb: [number, number, number, number] = [
+ parseInt(r),
+ parseInt(g),
+ parseInt(b),
+ 255
+ ];
+ if (areColorsSimilar(matchRgb, fromColor, similarity)) {
+ return `rgb(${toColor[0]}, ${toColor[1]}, ${toColor[2]})`;
+ }
+ return match;
+ });
- // Replace named SVG colors if they match our target color
- const namedColors = {
- red: [255, 0, 0],
- green: [0, 128, 0],
- blue: [0, 0, 255],
- black: [0, 0, 0],
- white: [255, 255, 255]
- // Add more named colors as needed
+ const rgbaRegex = new RegExp(
+ `rgba\\(\\s*(\\d+)\\s*,\\s*(\\d+)\\s*,\\s*(\\d+)\\s*,\\s*([\\d.]+)\\s*\\)`,
+ 'gi'
+ );
+ svgContent = svgContent.replace(rgbaRegex, (match, r, g, b, a) => {
+ const matchRgba: [number, number, number, number] = [
+ parseInt(r),
+ parseInt(g),
+ parseInt(b),
+ Math.round(parseFloat(a) * 255)
+ ];
+ if (areColorsSimilar(matchRgba, fromColor, similarity)) {
+ return `rgba(${toColor[0]}, ${toColor[1]}, ${toColor[2]}, ${
+ toColor[3] / 255
+ })`;
+ }
+ return match;
+ });
+
+ const namedColors: Record = {
+ red: [255, 0, 0, 255],
+ green: [0, 128, 0, 255],
+ blue: [0, 0, 255, 255],
+ black: [0, 0, 0, 255],
+ white: [255, 255, 255, 255]
+ };
+
+ Object.entries(namedColors).forEach(([name, rgba]) => {
+ if (areColorsSimilar(rgba, fromColor, similarity)) {
+ const colorRegex = new RegExp(`\\b${name}\\b`, 'gi');
+ svgContent = svgContent.replace(colorRegex, toColorHex);
+ }
+ });
+
+ resolve(new File([svgContent], file.name, { type: 'image/svg+xml' }));
};
-
- Object.entries(namedColors).forEach(([name, rgb]) => {
- if (
- areColorsSimilar(
- rgb as [number, number, number],
- fromColor,
- similarity
- )
- ) {
- const colorRegex = new RegExp(`\\b${name}\\b`, 'gi');
- svgContent = svgContent.replace(colorRegex, toColorHex);
- }
- });
-
- // Create new file with modified content
- const newFile = new File([svgContent], file.name, {
- type: 'image/svg+xml'
- });
- setResult(newFile);
- };
- reader.readAsText(file);
- return;
+ reader.readAsText(file);
+ });
}
+
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
- if (ctx == null) return;
- const img = new Image();
+ if (!ctx) throw new Error('Canvas not supported');
- img.src = URL.createObjectURL(file);
+ const img = new Image();
+ const url = URL.createObjectURL(file);
+ img.src = url;
await img.decode();
+ URL.revokeObjectURL(url);
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img, 0, 0);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
- const data: Uint8ClampedArray = imageData.data;
+ const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
- const currentColor: [number, number, number] = [
+ const currentColor: [number, number, number, number] = [
data[i],
data[i + 1],
- data[i + 2]
+ data[i + 2],
+ data[i + 3]
];
if (areColorsSimilar(currentColor, fromColor, similarity)) {
- data[i] = toColor[0]; // Red
- data[i + 1] = toColor[1]; // Green
- data[i + 2] = toColor[2]; // Blue
+ data[i] = toColor[0];
+ data[i + 1] = toColor[1];
+ data[i + 2] = toColor[2];
+ data[i + 3] = toColor[3];
}
}
ctx.putImageData(imageData, 0, 0);
- canvas.toBlob((blob) => {
- if (blob) {
- const newFile = new File([blob], file.name, {
- type: file.type
- });
- setResult(newFile);
- }
- }, file.type);
+ return new Promise((resolve, reject) => {
+ canvas.toBlob((blob) => {
+ if (blob) resolve(new File([blob], file.name, { type: file.type }));
+ else reject(new Error('Failed to create blob'));
+ }, file.type);
+ });
};
-const rgbToHex = (r: number, g: number, b: number): string => {
- return (
- '#' +
- [r, g, b]
- .map((x) => {
- const hex = x.toString(16);
- return hex.length === 1 ? '0' + hex : hex;
- })
- .join('')
- );
-};
+const rgbToHex = (r: number, g: number, b: number, a = 255): string =>
+ '#' + [r, g, b, a].map((x) => x.toString(16).padStart(2, '0')).join('');
-// Helper function to parse hex to RGB
-const hexToRgb = (hex: string): [number, number, number] | null => {
+const hexToRgb = (hex: string): [number, number, number, number] | null => {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result
? [
parseInt(result[1], 16),
parseInt(result[2], 16),
- parseInt(result[3], 16)
+ parseInt(result[3], 16),
+ 255
+ ]
+ : null;
+};
+
+const hexToRgba = (hex: string): [number, number, number, number] | null => {
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(
+ hex
+ );
+ return result
+ ? [
+ parseInt(result[1], 16),
+ parseInt(result[2], 16),
+ parseInt(result[3], 16),
+ parseInt(result[4], 16)
]
: null;
};
diff --git a/src/pages/tools/image/generic/change-colors/types.ts b/src/pages/tools/image/generic/change-colors/types.ts
new file mode 100644
index 0000000..9060a05
--- /dev/null
+++ b/src/pages/tools/image/generic/change-colors/types.ts
@@ -0,0 +1,5 @@
+export type InitialValuesType = {
+ fromColor: string;
+ toColor: string;
+ similarity: number;
+};
diff --git a/src/pages/tools/image/generic/create-transparent/index.tsx b/src/pages/tools/image/generic/create-transparent/index.tsx
index ce91a07..2756db9 100644
--- a/src/pages/tools/image/generic/create-transparent/index.tsx
+++ b/src/pages/tools/image/generic/create-transparent/index.tsx
@@ -13,7 +13,8 @@ import { GetGroupsType } from '@components/options/ToolOptions';
const initialValues = {
fromColor: 'white',
- similarity: '10'
+ similarity: '10',
+ backgroundColor: '#ffffff' // NEW
};
const validationSchema = Yup.object({
@@ -26,12 +27,16 @@ export default function CreateTransparent({ title }: ToolComponentProps) {
const compute = (optionsValues: typeof initialValues, input: any) => {
if (!input) return;
- const { fromColor, similarity } = optionsValues;
+ const { fromColor, similarity, backgroundColor } = optionsValues;
let fromRgb: [number, number, number];
+ let bgRgb: [number, number, number];
+
try {
//@ts-ignore
fromRgb = Color(fromColor).rgb().array();
+ //@ts-ignore
+ bgRgb = Color(backgroundColor).rgb().array();
} catch (err) {
return;
}
@@ -62,7 +67,12 @@ export default function CreateTransparent({ title }: ToolComponentProps) {
data[i + 2]
];
if (areColorsSimilar(currentColor, fromColor, similarity)) {
- data[i + 3] = 0; // Set alpha to 0 (transparent)
+ // data[i + 3] = 0; // Set alpha to 0 (transparent)
+ // NEW LOGIC: replace with background color
+ data[i] = bgRgb[0];
+ data[i + 1] = bgRgb[1];
+ data[i + 2] = bgRgb[2];
+ data[i + 3] = 255;
}
}
@@ -100,6 +110,13 @@ export default function CreateTransparent({ title }: ToolComponentProps) {
'Match this % of similar colors of the from color. For example, 10% white will match white and a little bit of gray.'
}
/>
+
+ {/* NEW FEATURE */}
+ updateField('backgroundColor', val)}
+ description={'Background color for replaced pixels'}
+ />
)
}
diff --git a/src/pages/tools/image/generic/index.ts b/src/pages/tools/image/generic/index.ts
index 6fa51c6..bb39bd8 100644
--- a/src/pages/tools/image/generic/index.ts
+++ b/src/pages/tools/image/generic/index.ts
@@ -1,3 +1,4 @@
+import { tool as splitImage } from './split/meta';
import { tool as resizeImage } from './resize/meta';
import { tool as compressImage } from './compress/meta';
import { tool as changeColors } from './change-colors/meta';
@@ -21,5 +22,6 @@ export const imageGenericTools = [
createTransparent,
imageToText,
qrCodeGenerator,
- rotateImage
+ rotateImage,
+ splitImage
];
diff --git a/src/pages/tools/image/generic/split/index.tsx b/src/pages/tools/image/generic/split/index.tsx
new file mode 100644
index 0000000..5bb7bd9
--- /dev/null
+++ b/src/pages/tools/image/generic/split/index.tsx
@@ -0,0 +1,176 @@
+import { Box } from '@mui/material';
+import { useState, useCallback } from 'react';
+import ToolContent from '@components/ToolContent';
+import { ToolComponentProps } from '@tools/defineTool';
+import { GetGroupsType } from '@components/options/ToolOptions';
+import { InitialValuesType } from './types';
+import ToolImageInput from '@components/input/ToolImageInput';
+import { t } from 'i18next';
+import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
+import { updateNumberField } from '@utils/string';
+import { splitImagesToPDF, fromPts, toPts } from './service';
+import { debounce } from 'lodash';
+import { PageSizes } from 'pdf-lib';
+import ToolFileResult from '@components/result/ToolFileResult';
+import SelectWithDesc from '@components/options/SelectWithDesc';
+
+const initialValues: InitialValuesType = {
+ pageFormat: 'A4',
+ pageWidth: 210,
+ pageHeight: 297,
+ pxPerSquareQuantity: 1620,
+ squareQuantity: 20,
+ unitsPerOneSquare: fromPts(72, 'mm'),
+ unitKind: 'mm',
+ padding: 5
+};
+
+export default function Split({ title, longDescription }: ToolComponentProps) {
+ const [input, setInput] = useState(null);
+ const [result, setResult] = useState(null);
+ const [isProcessing, setIsProcessing] = useState(false);
+
+ const compute = async (values: InitialValuesType, input: File | null) => {
+ if (!input) return;
+
+ setIsProcessing(true);
+
+ try {
+ const pdfFile = await splitImagesToPDF(input, values);
+ setResult(pdfFile);
+ } finally {
+ setIsProcessing(false);
+ }
+ };
+
+ const debouncedCompute = useCallback(debounce(compute, 1000), []);
+
+ const getGroups: GetGroupsType | null = ({
+ values,
+ updateField
+ }) => [
+ {
+ title: t('image:split.pageParameters.title'),
+ component: (
+
+ {
+ updateField('pageFormat', val as keyof typeof PageSizes);
+ }}
+ description={t('image:split.pageParameters.selectPageFormat')}
+ options={Object.keys(PageSizes).map((key) => ({
+ value: key,
+ label: key
+ }))}
+ />
+ {
+ updateNumberField(value, 'padding', updateField);
+ }}
+ value={values.padding}
+ />
+
+ )
+ },
+ {
+ title: t('image:split.scaleParameters.title'),
+ component: (
+
+ {
+ updateNumberField(value, 'pxPerSquareQuantity', updateField);
+ }}
+ value={values.pxPerSquareQuantity}
+ />
+ {
+ updateNumberField(value, 'squareQuantity', updateField);
+ }}
+ value={values.squareQuantity}
+ />
+ {
+ updateNumberField(value, 'unitsPerOneSquare', updateField);
+ }}
+ value={values.unitsPerOneSquare}
+ />
+
+ )
+ },
+ {
+ title: t('image:split.units.title'),
+ component: (
+
+ {
+ updateField('unitKind', newUnitKind);
+ updateField('unitsPerOneSquare', fromPts(72, newUnitKind));
+ updateField(
+ 'padding',
+ Math.round(fromPts(toPts(5, 'mm'), newUnitKind) * 100) / 100
+ );
+ }}
+ description={t('image:split.units.select')}
+ options={[
+ { value: 'pt', label: t('image:split.units.options.pt') },
+ { value: 'mm', label: t('image:split.units.options.mm') },
+ { value: 'in', label: t('image:split.units.options.in') }
+ ]}
+ />
+
+ )
+ }
+ ];
+ return (
+
+ }
+ resultComponent={
+
+ }
+ initialValues={initialValues}
+ getGroups={getGroups}
+ setInput={setInput}
+ compute={debouncedCompute}
+ toolInfo={{ title: `What is a ${title}?`, description: longDescription }}
+ />
+ );
+}
diff --git a/src/pages/tools/image/generic/split/meta.ts b/src/pages/tools/image/generic/split/meta.ts
new file mode 100644
index 0000000..45dd6f0
--- /dev/null
+++ b/src/pages/tools/image/generic/split/meta.ts
@@ -0,0 +1,14 @@
+import { defineTool } from '@tools/defineTool';
+import { lazy } from 'react';
+
+export const tool = defineTool('image-generic', {
+ i18n: {
+ name: 'image:split.title',
+ description: 'image:split.description',
+ shortDescription: 'image:split.shortDescription'
+ },
+ path: 'split-to-pages',
+ icon: 'mdi:scissors',
+ keywords: ['split', 'images', 'pages', 'large', 'map'],
+ component: lazy(() => import('./index'))
+});
diff --git a/src/pages/tools/image/generic/split/service.ts b/src/pages/tools/image/generic/split/service.ts
new file mode 100644
index 0000000..2408987
--- /dev/null
+++ b/src/pages/tools/image/generic/split/service.ts
@@ -0,0 +1,136 @@
+import { InitialValuesType, units } from './types';
+import { PDFDocument, PageSizes } from 'pdf-lib';
+
+async function splitImage(
+ initImg: HTMLImageElement,
+ widthOfEachPart: number,
+ heightOfEachPart: number
+): Promise {
+ const { width, height } = initImg;
+ const horParts = Math.ceil(width / widthOfEachPart);
+ const verParts = Math.ceil(height / heightOfEachPart);
+
+ const promises: Promise[] = [];
+
+ for (let y = 0; y < verParts; y++) {
+ for (let x = 0; x < horParts; x++) {
+ const tileW = Math.min(widthOfEachPart, width - x * widthOfEachPart);
+ const tileH = Math.min(heightOfEachPart, height - y * heightOfEachPart);
+
+ const canvas = document.createElement('canvas');
+ canvas.width = tileW;
+ canvas.height = tileH;
+
+ const ctx = canvas.getContext('2d');
+ if (!ctx) throw new Error('Canvas not supported');
+
+ ctx.drawImage(
+ initImg,
+ x * widthOfEachPart,
+ y * heightOfEachPart,
+ tileW,
+ tileH,
+ 0,
+ 0,
+ tileW,
+ tileH
+ );
+
+ promises.push(canvasToFile(canvas, `part-${x}-${y}.png`));
+ }
+ }
+
+ return Promise.all(promises);
+}
+
+export async function splitImagesToPDF(
+ input: File,
+ values: InitialValuesType
+): Promise {
+ const initImg = await loadImage(input);
+
+ const [pageWidth, pageHeight] = PageSizes[values.pageFormat];
+ const ptPerOneSquare = toPts(values.unitsPerOneSquare, values.unitKind);
+ const pxPerPt =
+ values.pxPerSquareQuantity / (ptPerOneSquare * values.squareQuantity);
+ const paddingInPts = toPts(values.padding, values.unitKind);
+ const pageWidthWithPadding = pageWidth - 2 * paddingInPts;
+ const pageHeightWithPadding = pageHeight - 2 * paddingInPts;
+
+ const imgParts = await splitImage(
+ initImg,
+ Math.round(pageWidthWithPadding * pxPerPt),
+ Math.round(pageHeightWithPadding * pxPerPt)
+ );
+
+ const pdfDoc = await PDFDocument.create();
+
+ for (const imgFile of imgParts) {
+ const img = await pdfDoc.embedPng(await imgFile.arrayBuffer());
+ const page = pdfDoc.addPage([pageWidth, pageHeight]);
+ page.drawImage(img, {
+ x: paddingInPts,
+ y: paddingInPts,
+ width: pageWidthWithPadding,
+ height: pageHeightWithPadding
+ });
+ }
+
+ const pdfBytes = await pdfDoc.save();
+
+ return new File(
+ [pdfBytes as BlobPart],
+ input.name.replace(/\.([^.]+)?$/i, `-${Date.now()}.pdf`),
+ { type: 'application/pdf' }
+ );
+}
+
+export function loadImage(input: File): Promise {
+ return new Promise((resolve, reject) => {
+ const img = new Image();
+ const url = URL.createObjectURL(input);
+ img.src = url;
+ img.onload = () => {
+ URL.revokeObjectURL(url);
+ resolve(img);
+ };
+ img.onerror = (err) => {
+ URL.revokeObjectURL(url);
+ reject(err);
+ };
+ });
+}
+
+function canvasToFile(canvas: HTMLCanvasElement, name: string): Promise {
+ return new Promise((resolve, reject) => {
+ canvas.toBlob((blob) => {
+ if (blob) {
+ resolve(new File([blob], name, { type: 'image/png' }));
+ } else {
+ reject(new Error('Failed to create blob'));
+ }
+ }, 'image/png');
+ });
+}
+
+export function toPts(number: number, units: units): number {
+ switch (units) {
+ case 'pt':
+ return number;
+ case 'mm':
+ return (number * 72) / 25.4;
+ case 'in':
+ return number * 72;
+ }
+}
+
+export function fromPts(number: number, units: units): number {
+ switch (units) {
+ case 'pt':
+ return number;
+ case 'mm':
+ return (number * 25.4) / 72;
+ case 'in':
+ return number / 72;
+ }
+}
diff --git a/src/pages/tools/image/generic/split/types.ts b/src/pages/tools/image/generic/split/types.ts
new file mode 100644
index 0000000..0970a25
--- /dev/null
+++ b/src/pages/tools/image/generic/split/types.ts
@@ -0,0 +1,16 @@
+import { PageSizes } from 'pdf-lib';
+
+export type InitialValuesType = {
+ pageFormat: keyof typeof PageSizes;
+ widthOfEachPart?: number;
+ heightOfEachPart?: number;
+ pageWidth: number;
+ pageHeight: number;
+ pxPerSquareQuantity: number;
+ squareQuantity: number;
+ unitsPerOneSquare: number;
+ unitKind: units;
+ padding: number;
+};
+
+export type units = 'pt' | 'mm' | 'in';
diff --git a/src/utils/color.ts b/src/utils/color.ts
index 692a78e..37e73dd 100644
--- a/src/utils/color.ts
+++ b/src/utils/color.ts
@@ -1,24 +1,21 @@
export function areColorsSimilar(
- color1: [number, number, number],
- color2: [number, number, number],
+ color1: [number, number, number, number?],
+ color2: [number, number, number, number?],
similarity: number
): boolean {
- const colorDistance = (
- c1: [number, number, number],
- c2: [number, number, number]
- ) => {
- return Math.sqrt(
- Math.pow(c1[0] - c2[0], 2) +
- Math.pow(c1[1] - c2[1], 2) +
- Math.pow(c1[2] - c2[2], 2)
- );
- };
- const maxColorDistance = Math.sqrt(
- Math.pow(255, 2) + Math.pow(255, 2) + Math.pow(255, 2)
- );
- const similarityThreshold = (similarity / 100) * maxColorDistance;
+ const a1 = color1[3] ?? 255;
+ const a2 = color2[3] ?? 255;
- return colorDistance(color1, color2) <= similarityThreshold;
+ const distance = Math.sqrt(
+ Math.pow(color1[0] - color2[0], 2) +
+ Math.pow(color1[1] - color2[1], 2) +
+ Math.pow(color1[2] - color2[2], 2) +
+ Math.pow(a1 - a2, 2)
+ );
+
+ const maxDistance = Math.sqrt(Math.pow(255, 2) * 4);
+
+ return distance <= (similarity / 100) * maxDistance;
}
export function convertHexToRGBA(color: string): number {