mirror of
https://github.com/CorentinTh/it-tools.git
synced 2025-05-04 13:29:13 -04:00
feat(new tool): Potrace
Convert an raster image to vectorial SVG
This commit is contained in:
parent
b430baef40
commit
045075a051
7 changed files with 1849 additions and 64 deletions
2
components.d.ts
vendored
2
components.d.ts
vendored
|
@ -127,6 +127,7 @@ declare module '@vue/runtime-core' {
|
||||||
MetaTagGenerator: typeof import('./src/tools/meta-tag-generator/meta-tag-generator.vue')['default']
|
MetaTagGenerator: typeof import('./src/tools/meta-tag-generator/meta-tag-generator.vue')['default']
|
||||||
MimeTypes: typeof import('./src/tools/mime-types/mime-types.vue')['default']
|
MimeTypes: typeof import('./src/tools/mime-types/mime-types.vue')['default']
|
||||||
NavbarButtons: typeof import('./src/components/NavbarButtons.vue')['default']
|
NavbarButtons: typeof import('./src/components/NavbarButtons.vue')['default']
|
||||||
|
NCheckbox: typeof import('naive-ui')['NCheckbox']
|
||||||
NCode: typeof import('naive-ui')['NCode']
|
NCode: typeof import('naive-ui')['NCode']
|
||||||
NCollapseTransition: typeof import('naive-ui')['NCollapseTransition']
|
NCollapseTransition: typeof import('naive-ui')['NCollapseTransition']
|
||||||
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
|
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
|
||||||
|
@ -152,6 +153,7 @@ declare module '@vue/runtime-core' {
|
||||||
PdfSignatureDetails: typeof import('./src/tools/pdf-signature-checker/components/pdf-signature-details.vue')['default']
|
PdfSignatureDetails: typeof import('./src/tools/pdf-signature-checker/components/pdf-signature-details.vue')['default']
|
||||||
PercentageCalculator: typeof import('./src/tools/percentage-calculator/percentage-calculator.vue')['default']
|
PercentageCalculator: typeof import('./src/tools/percentage-calculator/percentage-calculator.vue')['default']
|
||||||
PhoneParserAndFormatter: typeof import('./src/tools/phone-parser-and-formatter/phone-parser-and-formatter.vue')['default']
|
PhoneParserAndFormatter: typeof import('./src/tools/phone-parser-and-formatter/phone-parser-and-formatter.vue')['default']
|
||||||
|
Potrace: typeof import('./src/tools/potrace/potrace.vue')['default']
|
||||||
QrCodeGenerator: typeof import('./src/tools/qr-code-generator/qr-code-generator.vue')['default']
|
QrCodeGenerator: typeof import('./src/tools/qr-code-generator/qr-code-generator.vue')['default']
|
||||||
RandomPortGenerator: typeof import('./src/tools/random-port-generator/random-port-generator.vue')['default']
|
RandomPortGenerator: typeof import('./src/tools/random-port-generator/random-port-generator.vue')['default']
|
||||||
ResultRow: typeof import('./src/tools/ipv4-range-expander/result-row.vue')['default']
|
ResultRow: typeof import('./src/tools/ipv4-range-expander/result-row.vue')['default']
|
||||||
|
|
|
@ -42,6 +42,7 @@
|
||||||
"@tiptap/starter-kit": "2.1.6",
|
"@tiptap/starter-kit": "2.1.6",
|
||||||
"@tiptap/vue-3": "2.0.3",
|
"@tiptap/vue-3": "2.0.3",
|
||||||
"@types/figlet": "^1.5.8",
|
"@types/figlet": "^1.5.8",
|
||||||
|
"@types/potrace": "^2.1.5",
|
||||||
"@vicons/material": "^0.12.0",
|
"@vicons/material": "^0.12.0",
|
||||||
"@vicons/tabler": "^0.12.0",
|
"@vicons/tabler": "^0.12.0",
|
||||||
"@vueuse/core": "^10.3.0",
|
"@vueuse/core": "^10.3.0",
|
||||||
|
@ -80,6 +81,7 @@
|
||||||
"pdf-signature-reader": "^1.4.2",
|
"pdf-signature-reader": "^1.4.2",
|
||||||
"pinia": "^2.0.34",
|
"pinia": "^2.0.34",
|
||||||
"plausible-tracker": "^0.3.8",
|
"plausible-tracker": "^0.3.8",
|
||||||
|
"potrace": "^2.1.8",
|
||||||
"qrcode": "^1.5.1",
|
"qrcode": "^1.5.1",
|
||||||
"sql-formatter": "^13.0.0",
|
"sql-formatter": "^13.0.0",
|
||||||
"ua-parser-js": "^1.0.35",
|
"ua-parser-js": "^1.0.35",
|
||||||
|
@ -132,6 +134,7 @@
|
||||||
"unplugin-icons": "^0.17.0",
|
"unplugin-icons": "^0.17.0",
|
||||||
"unplugin-vue-components": "^0.25.0",
|
"unplugin-vue-components": "^0.25.0",
|
||||||
"vite": "^4.4.9",
|
"vite": "^4.4.9",
|
||||||
|
"vite-plugin-node-polyfills": "^0.22.0",
|
||||||
"vite-plugin-pwa": "^0.16.0",
|
"vite-plugin-pwa": "^0.16.0",
|
||||||
"vite-plugin-vue-markdown": "^0.23.5",
|
"vite-plugin-vue-markdown": "^0.23.5",
|
||||||
"vite-svg-loader": "^4.0.0",
|
"vite-svg-loader": "^4.0.0",
|
||||||
|
|
1781
pnpm-lock.yaml
generated
1781
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
@ -6,6 +6,7 @@ import { tool as asciiTextDrawer } from './ascii-text-drawer';
|
||||||
|
|
||||||
import { tool as textToUnicode } from './text-to-unicode';
|
import { tool as textToUnicode } from './text-to-unicode';
|
||||||
import { tool as safelinkDecoder } from './safelink-decoder';
|
import { tool as safelinkDecoder } from './safelink-decoder';
|
||||||
|
import { tool as potrace } from './potrace';
|
||||||
import { tool as pdfSignatureChecker } from './pdf-signature-checker';
|
import { tool as pdfSignatureChecker } from './pdf-signature-checker';
|
||||||
import { tool as numeronymGenerator } from './numeronym-generator';
|
import { tool as numeronymGenerator } from './numeronym-generator';
|
||||||
import { tool as macAddressGenerator } from './mac-address-generator';
|
import { tool as macAddressGenerator } from './mac-address-generator';
|
||||||
|
@ -132,7 +133,13 @@ export const toolsByCategory: ToolCategory[] = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Images and videos',
|
name: 'Images and videos',
|
||||||
components: [qrCodeGenerator, wifiQrCodeGenerator, svgPlaceholderGenerator, cameraRecorder],
|
components: [
|
||||||
|
qrCodeGenerator,
|
||||||
|
wifiQrCodeGenerator,
|
||||||
|
svgPlaceholderGenerator,
|
||||||
|
cameraRecorder,
|
||||||
|
potrace,
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Development',
|
name: 'Development',
|
||||||
|
|
12
src/tools/potrace/index.ts
Normal file
12
src/tools/potrace/index.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import { ArrowsShuffle } from '@vicons/tabler';
|
||||||
|
import { defineTool } from '../tool';
|
||||||
|
|
||||||
|
export const tool = defineTool({
|
||||||
|
name: 'Image to SVG (potrace)',
|
||||||
|
path: '/potrace',
|
||||||
|
description: 'Convert an raster image to vectorial SVG',
|
||||||
|
keywords: ['potrace', 'image', 'svg', 'raster', 'vectorial'],
|
||||||
|
component: () => import('./potrace.vue'),
|
||||||
|
icon: ArrowsShuffle,
|
||||||
|
createdAt: new Date('2024-05-11'),
|
||||||
|
});
|
104
src/tools/potrace/potrace.vue
Normal file
104
src/tools/potrace/potrace.vue
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Buffer } from 'node:buffer';
|
||||||
|
import type { Ref } from 'vue';
|
||||||
|
import potrace from 'potrace';
|
||||||
|
import { Base64 } from 'js-base64';
|
||||||
|
import TextareaCopyable from '@/components/TextareaCopyable.vue';
|
||||||
|
|
||||||
|
async function traceAsync(input: Buffer) {
|
||||||
|
return new Promise<string>((resolve, reject) => {
|
||||||
|
potrace.trace(input, (err: Error | null, svg: string) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
resolve(svg);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function posterizeAsync(input: Buffer) {
|
||||||
|
return new Promise<string>((resolve, reject) => {
|
||||||
|
potrace.posterize(input,
|
||||||
|
(err: Error | null, svg: string) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
resolve(svg);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function file2Buffer(file: File) {
|
||||||
|
return new Promise<Buffer>((resolve, _reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.addEventListener('load', () => {
|
||||||
|
const buffer = Buffer.from(reader.result as ArrayBuffer);
|
||||||
|
resolve(buffer);
|
||||||
|
});
|
||||||
|
reader.readAsArrayBuffer(file);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const posterize = ref(true);
|
||||||
|
const fileInput = ref() as Ref<File>;
|
||||||
|
const svg = computedAsync(async () => {
|
||||||
|
const trace = !posterize.value;
|
||||||
|
const file = fileInput.value;
|
||||||
|
if (!file) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const buffer = await file2Buffer(file);
|
||||||
|
return (trace ? await traceAsync(buffer) : await posterizeAsync(buffer));
|
||||||
|
}
|
||||||
|
catch (e: any) {
|
||||||
|
return e.toString();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const svgBase64 = computed(() => svg.value ? `data:image/svg+xml;base64,${Base64.encode(svg.value)}` : '');
|
||||||
|
|
||||||
|
async function onUpload(file: File) {
|
||||||
|
if (file) {
|
||||||
|
fileInput.value = file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div style="max-width: 600px;">
|
||||||
|
<c-file-upload
|
||||||
|
title="Drag and drop an image here, or click to select a file"
|
||||||
|
:paste-image="true"
|
||||||
|
@file-upload="onUpload"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div style="text-align: center;">
|
||||||
|
<n-checkbox v-model:checked="posterize" mt-2>
|
||||||
|
Posterize?
|
||||||
|
</n-checkbox>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<n-divider />
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3>Potrace result</h3>
|
||||||
|
<TextareaCopyable
|
||||||
|
:value="svg"
|
||||||
|
word-wrap
|
||||||
|
/>
|
||||||
|
|
||||||
|
<n-divider />
|
||||||
|
|
||||||
|
<div style="text-align: center;">
|
||||||
|
<img width="150" :src="svgBase64" style="background-color: white">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
::v-deep(.n-upload-trigger) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -15,6 +15,7 @@ import { VitePWA } from 'vite-plugin-pwa';
|
||||||
import markdown from 'vite-plugin-vue-markdown';
|
import markdown from 'vite-plugin-vue-markdown';
|
||||||
import svgLoader from 'vite-svg-loader';
|
import svgLoader from 'vite-svg-loader';
|
||||||
import { configDefaults } from 'vitest/config';
|
import { configDefaults } from 'vitest/config';
|
||||||
|
import { nodePolyfills } from 'vite-plugin-node-polyfills'
|
||||||
|
|
||||||
const baseUrl = process.env.BASE_URL ?? '/';
|
const baseUrl = process.env.BASE_URL ?? '/';
|
||||||
|
|
||||||
|
@ -97,6 +98,7 @@ export default defineConfig({
|
||||||
resolvers: [NaiveUiResolver(), IconsResolver({ prefix: 'icon' })],
|
resolvers: [NaiveUiResolver(), IconsResolver({ prefix: 'icon' })],
|
||||||
}),
|
}),
|
||||||
Unocss(),
|
Unocss(),
|
||||||
|
nodePolyfills(),
|
||||||
],
|
],
|
||||||
base: baseUrl,
|
base: baseUrl,
|
||||||
resolve: {
|
resolve: {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue