Merge branch 'main' into pr/Sushanth012/325
This commit is contained in:
@@ -94,7 +94,10 @@
|
||||
"splitText": "Einen Text teilen",
|
||||
"trimVideo": "Video zuschneiden"
|
||||
},
|
||||
"searchPlaceholder": "Alle Tools durchsuchen",
|
||||
"search": {
|
||||
"placeholder": "Alle Tools durchsuchen",
|
||||
"noResult": "Keine Ergebnisse gefunden, versuchen Sie Ihre Suche oder Filterkriterien anzupassen"
|
||||
},
|
||||
"title": "Erledigen Sie Dinge schnell mit"
|
||||
},
|
||||
"inputFooter": {
|
||||
|
||||
@@ -2,7 +2,22 @@
|
||||
"changeColors": {
|
||||
"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.",
|
||||
@@ -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)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,7 +106,10 @@
|
||||
"splitText": "Split a text",
|
||||
"trimVideo": "Trim video"
|
||||
},
|
||||
"searchPlaceholder": "Search all tools",
|
||||
"search": {
|
||||
"placeholder": "Search all tools",
|
||||
"noResult": "No results found, Try adjusting your search or filter criteria"
|
||||
},
|
||||
"title": "Get Things Done Quickly with"
|
||||
},
|
||||
"inputFooter": {
|
||||
|
||||
@@ -94,7 +94,10 @@
|
||||
"splitText": "Dividir un texto",
|
||||
"trimVideo": "Recortar vídeo"
|
||||
},
|
||||
"searchPlaceholder": "Buscar en todas las herramientas",
|
||||
"search": {
|
||||
"placeholder": "Buscar todas las herramientas",
|
||||
"noResult": "No se encontraron resultados, intenta ajustar tu búsqueda o los criterios de filtro"
|
||||
},
|
||||
"title": "Haga las cosas rápidamente con"
|
||||
},
|
||||
"inputFooter": {
|
||||
|
||||
@@ -94,7 +94,10 @@
|
||||
"splitText": "Diviser un texte",
|
||||
"trimVideo": "Découper la vidéo"
|
||||
},
|
||||
"searchPlaceholder": "Rechercher tous les outils",
|
||||
"search": {
|
||||
"placeholder": "Rechercher tous les outils",
|
||||
"noResult": "Aucun résultat trouvé, essayez d'ajuster votre recherche ou vos critères de filtre"
|
||||
},
|
||||
"title": "Faites avancer les choses rapidement avec"
|
||||
},
|
||||
"inputFooter": {
|
||||
|
||||
@@ -94,7 +94,10 @@
|
||||
"splitText": "टेक्स्ट विभाजित करें",
|
||||
"trimVideo": "वीडियो ट्रिम करें"
|
||||
},
|
||||
"searchPlaceholder": "सभी टूल्स खोजें",
|
||||
"search": {
|
||||
"placeholder": "सभी टूल खोजें",
|
||||
"noResult": "कोई परिणाम नहीं मिला, अपनी खोज या फ़िल्टर मानदंड समायोजित करने का प्रयास करें"
|
||||
},
|
||||
"title": "के साथ जल्दी काम करें"
|
||||
},
|
||||
"inputFooter": {
|
||||
|
||||
@@ -94,7 +94,10 @@
|
||||
"splitText": "テキストを分割する",
|
||||
"trimVideo": "ビデオをトリミングする"
|
||||
},
|
||||
"searchPlaceholder": "すべてのツールを検索",
|
||||
"search": {
|
||||
"placeholder": "すべてのツールを検索",
|
||||
"noResult": "結果が見つかりません。検索条件またはフィルター条件を調整してみてください"
|
||||
},
|
||||
"title": "物事を素早く終わらせる"
|
||||
},
|
||||
"inputFooter": {
|
||||
|
||||
@@ -94,7 +94,10 @@
|
||||
"splitText": "Een tekst splitsen",
|
||||
"trimVideo": "Video bijsnijden"
|
||||
},
|
||||
"searchPlaceholder": "Zoek in alle tools",
|
||||
"search": {
|
||||
"placeholder": "Alle tools doorzoeken",
|
||||
"noResult": "Geen resultaten gevonden, probeer uw zoekopdracht of filtercriteria aan te passen"
|
||||
},
|
||||
"title": "Krijg dingen snel gedaan met"
|
||||
},
|
||||
"inputFooter": {
|
||||
|
||||
@@ -94,7 +94,10 @@
|
||||
"splitText": "Dividir um texto",
|
||||
"trimVideo": "Cortar vídeo"
|
||||
},
|
||||
"searchPlaceholder": "Pesquisar todas as ferramentas",
|
||||
"search": {
|
||||
"placeholder": "Pesquisar todas as ferramentas",
|
||||
"noResult": "Nenhum resultado encontrado, tente ajustar a sua pesquisa ou os critérios de filtro"
|
||||
},
|
||||
"title": "Faça as coisas rapidamente com"
|
||||
},
|
||||
"inputFooter": {
|
||||
|
||||
@@ -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)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,7 +94,10 @@
|
||||
"splitText": "Разделить текст",
|
||||
"trimVideo": "Обрезать видео"
|
||||
},
|
||||
"searchPlaceholder": "Найти инструмент…",
|
||||
"search": {
|
||||
"placeholder": "Поиск по всем инструментам",
|
||||
"noResult": "Результаты не найдены, попробуйте изменить параметры поиска или фильтрации"
|
||||
},
|
||||
"title": "Выполняйте задачи быстро с помощью"
|
||||
},
|
||||
"inputFooter": {
|
||||
|
||||
@@ -98,7 +98,10 @@
|
||||
"splitText": "拆分文本",
|
||||
"trimVideo": "修剪视频"
|
||||
},
|
||||
"searchPlaceholder": "搜索所有工具",
|
||||
"search": {
|
||||
"placeholder": "搜索所有工具",
|
||||
"noResult": "未找到结果 请尝试调整搜索或筛选条件"
|
||||
},
|
||||
"title": "快速完成工作"
|
||||
},
|
||||
"inputFooter": {
|
||||
@@ -223,6 +226,10 @@
|
||||
"toolLayout": {
|
||||
"allToolsTitle": "全部 {{type}}"
|
||||
},
|
||||
"toolsByCategory": {
|
||||
"noResultsTitle": "未找到工具",
|
||||
"noResultsDescription": "请尝试调整搜索条件或筛选条件"
|
||||
},
|
||||
"toolMultiFileResult": {
|
||||
"copied": "文件已复制",
|
||||
"copyFailed": "复制失败: {{error}}",
|
||||
|
||||
@@ -150,6 +150,7 @@ export default function Hero() {
|
||||
sx={{ mb: 2 }}
|
||||
autoHighlight
|
||||
options={filteredTools}
|
||||
noOptionsText={t('translation:hero.search.noResult')}
|
||||
// Disable default MUI filtering since we already apply custom filterTools
|
||||
filterOptions={(options) => options}
|
||||
groupBy={(option) => option.type}
|
||||
@@ -167,7 +168,7 @@ export default function Hero() {
|
||||
<TextField
|
||||
{...params}
|
||||
fullWidth
|
||||
placeholder={t('translation:hero.searchPlaceholder')}
|
||||
placeholder={t('translation:hero.search.placeholder')}
|
||||
InputProps={{
|
||||
...params.InputProps,
|
||||
endAdornment: <SearchIcon />,
|
||||
|
||||
@@ -88,7 +88,8 @@ export default function ToolsByCategory() {
|
||||
</Typography>
|
||||
</Stack>
|
||||
<TextField
|
||||
placeholder={'Search'}
|
||||
placeholder={t('translation:hero.search.placeholder')}
|
||||
aria-label={t('translation:hero.search.placeholder')}
|
||||
InputProps={{
|
||||
endAdornment: <SearchIcon />,
|
||||
sx: {
|
||||
|
||||
@@ -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<File | null>(null);
|
||||
const [result, setResult] = useState<File | null>(null);
|
||||
const [isProcessing, setIsProcessing] = useState<boolean>(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<typeof initialValues> = ({
|
||||
const getGroups: GetGroupsType<InitialValuesType> = ({
|
||||
values,
|
||||
updateField
|
||||
}) => [
|
||||
{
|
||||
title: 'From color and to color',
|
||||
title: t('changeColors.fromColor.title'),
|
||||
component: (
|
||||
<Box>
|
||||
<ColorSelector
|
||||
value={values.fromColor}
|
||||
onColorChange={(val) => updateField('fromColor', val)}
|
||||
description={'Replace this color (from color)'}
|
||||
inputProps={{ 'data-testid': 'from-color-input' }}
|
||||
<SimpleRadio
|
||||
title={t('changeColors.fromColor.transparentOption')}
|
||||
checked={values.fromColor === '#00000000'}
|
||||
onClick={() => updateField('fromColor', '#00000000')}
|
||||
/>
|
||||
<ColorSelector
|
||||
value={values.toColor}
|
||||
onColorChange={(val) => updateField('toColor', val)}
|
||||
description={'With this color (to color)'}
|
||||
inputProps={{ 'data-testid': 'to-color-input' }}
|
||||
<SimpleRadio
|
||||
title={t('changeColors.fromColor.colorOption')}
|
||||
checked={values.fromColor !== '#00000000'}
|
||||
onClick={() => updateField('fromColor', '#ffffffff')}
|
||||
/>
|
||||
{values.fromColor !== '#00000000' && (
|
||||
<ColorSelector
|
||||
value={values.fromColor}
|
||||
onColorChange={(val) => updateField('fromColor', val)}
|
||||
description={t('changeColors.fromColor.selectorDescription')}
|
||||
inputProps={{ 'data-testid': 'from-color-input' }}
|
||||
/>
|
||||
)}
|
||||
<TextFieldWithDesc
|
||||
value={values.similarity}
|
||||
onOwnChange={(val) => 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')}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t('changeColors.toColor.title'),
|
||||
component: (
|
||||
<Box>
|
||||
<SimpleRadio
|
||||
title={t('changeColors.toColor.transparentOption')}
|
||||
checked={values.toColor === '#00000000'}
|
||||
onClick={() => updateField('toColor', '#00000000')}
|
||||
/>
|
||||
<SimpleRadio
|
||||
title={t('changeColors.toColor.colorOption')}
|
||||
checked={values.toColor !== '#00000000'}
|
||||
onClick={() => updateField('toColor', '#000000ff')}
|
||||
/>
|
||||
{values.toColor !== '#00000000' && (
|
||||
<ColorSelector
|
||||
value={values.toColor}
|
||||
onColorChange={(val) => updateField('toColor', val)}
|
||||
description={t('changeColors.toColor.selectorDescription')}
|
||||
inputProps={{ 'data-testid': 'to-color-input' }}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<ToolContent
|
||||
title={title}
|
||||
@@ -84,10 +140,16 @@ export default function ChangeColorsInImage({ title }: ToolComponentProps) {
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
accept={['image/*']}
|
||||
title={'Input image'}
|
||||
title={t('changeColors.inputTitle')}
|
||||
/>
|
||||
}
|
||||
resultComponent={
|
||||
<ToolFileResult
|
||||
title={t('changeColors.resultTitle')}
|
||||
value={result}
|
||||
loading={isProcessing}
|
||||
/>
|
||||
}
|
||||
resultComponent={<ToolFileResult title={'Result image'} value={result} />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<void> => {
|
||||
fromColor: [number, number, number, number],
|
||||
toColor: [number, number, number, number],
|
||||
similarity: number
|
||||
): Promise<File> => {
|
||||
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<string, [number, number, number, number]> = {
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
export type InitialValuesType = {
|
||||
fromColor: string;
|
||||
toColor: string;
|
||||
similarity: number;
|
||||
};
|
||||
@@ -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 */}
|
||||
<ColorSelector
|
||||
value={values.backgroundColor}
|
||||
onColorChange={(val) => updateField('backgroundColor', val)}
|
||||
description={'Background color for replaced pixels'}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
];
|
||||
|
||||
@@ -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<File | null>(null);
|
||||
const [result, setResult] = useState<File | null>(null);
|
||||
const [isProcessing, setIsProcessing] = useState<boolean>(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<InitialValuesType> | null = ({
|
||||
values,
|
||||
updateField
|
||||
}) => [
|
||||
{
|
||||
title: t('image:split.pageParameters.title'),
|
||||
component: (
|
||||
<Box>
|
||||
<SelectWithDesc
|
||||
selected={values.pageFormat}
|
||||
onChange={(val) => {
|
||||
updateField('pageFormat', val as keyof typeof PageSizes);
|
||||
}}
|
||||
description={t('image:split.pageParameters.selectPageFormat')}
|
||||
options={Object.keys(PageSizes).map((key) => ({
|
||||
value: key,
|
||||
label: key
|
||||
}))}
|
||||
/>
|
||||
<TextFieldWithDesc
|
||||
name="padding"
|
||||
type="number"
|
||||
inputProps={{ min: 0, step: 1 }}
|
||||
description={t('image:split.pageParameters.padding.description')}
|
||||
onOwnChange={(value) => {
|
||||
updateNumberField(value, 'padding', updateField);
|
||||
}}
|
||||
value={values.padding}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t('image:split.scaleParameters.title'),
|
||||
component: (
|
||||
<Box>
|
||||
<TextFieldWithDesc
|
||||
name="pxPerSquareQuantity"
|
||||
type="number"
|
||||
inputProps={{ min: 1, step: 1 }}
|
||||
description={t(
|
||||
'image:split.scaleParameters.pxPerSquareQuantity.description'
|
||||
)}
|
||||
onOwnChange={(value) => {
|
||||
updateNumberField(value, 'pxPerSquareQuantity', updateField);
|
||||
}}
|
||||
value={values.pxPerSquareQuantity}
|
||||
/>
|
||||
<TextFieldWithDesc
|
||||
name="squareQuantity"
|
||||
type="number"
|
||||
inputProps={{ min: 1, step: 1 }}
|
||||
description={t(
|
||||
'image:split.scaleParameters.squareQuantity.description'
|
||||
)}
|
||||
onOwnChange={(value) => {
|
||||
updateNumberField(value, 'squareQuantity', updateField);
|
||||
}}
|
||||
value={values.squareQuantity}
|
||||
/>
|
||||
<TextFieldWithDesc
|
||||
name="mmPerOneSquare"
|
||||
type="number"
|
||||
inputProps={{ min: 1, step: 1 }}
|
||||
description={t(
|
||||
'image:split.scaleParameters.unitsPerOneSquare.description'
|
||||
)}
|
||||
onOwnChange={(value) => {
|
||||
updateNumberField(value, 'unitsPerOneSquare', updateField);
|
||||
}}
|
||||
value={values.unitsPerOneSquare}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t('image:split.units.title'),
|
||||
component: (
|
||||
<Box>
|
||||
<SelectWithDesc
|
||||
selected={values.unitKind}
|
||||
onChange={(newUnitKind) => {
|
||||
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') }
|
||||
]}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
];
|
||||
return (
|
||||
<ToolContent
|
||||
title={t('image:split.title')}
|
||||
input={input}
|
||||
inputComponent={
|
||||
<ToolImageInput
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
accept={['image/*']}
|
||||
title={t('image:split.inputTitle')}
|
||||
/>
|
||||
}
|
||||
resultComponent={
|
||||
<ToolFileResult
|
||||
title={t('image:split.resultTitle')}
|
||||
extension={'pdf'}
|
||||
value={result}
|
||||
loading={isProcessing}
|
||||
loadingText={t('image:split.loadingText')}
|
||||
/>
|
||||
}
|
||||
initialValues={initialValues}
|
||||
getGroups={getGroups}
|
||||
setInput={setInput}
|
||||
compute={debouncedCompute}
|
||||
toolInfo={{ title: `What is a ${title}?`, description: longDescription }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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'))
|
||||
});
|
||||
@@ -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<File[]> {
|
||||
const { width, height } = initImg;
|
||||
const horParts = Math.ceil(width / widthOfEachPart);
|
||||
const verParts = Math.ceil(height / heightOfEachPart);
|
||||
|
||||
const promises: Promise<File>[] = [];
|
||||
|
||||
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<File> {
|
||||
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<HTMLImageElement> {
|
||||
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<File> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
+14
-17
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user