feat(new tool): Potrace
Convert an raster image to vectorial SVG
This commit is contained in:
		
							parent
							
								
									b430baef40
								
							
						
					
					
						commit
						045075a051
					
				
							
								
								
									
										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…
	
		Reference in New Issue
	
	Block a user