diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..21cece4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/upload.gradle +/gradle.properties +/build +/cer \ No newline at end of file diff --git a/.gradle/7.4/checksums/checksums.lock b/.gradle/7.4/checksums/checksums.lock new file mode 100644 index 0000000..d5c66a6 Binary files /dev/null and b/.gradle/7.4/checksums/checksums.lock differ diff --git a/.gradle/7.4/checksums/md5-checksums.bin b/.gradle/7.4/checksums/md5-checksums.bin new file mode 100644 index 0000000..f685c72 Binary files /dev/null and b/.gradle/7.4/checksums/md5-checksums.bin differ diff --git a/.gradle/7.4/checksums/sha1-checksums.bin b/.gradle/7.4/checksums/sha1-checksums.bin new file mode 100644 index 0000000..b85d984 Binary files /dev/null and b/.gradle/7.4/checksums/sha1-checksums.bin differ diff --git a/.gradle/7.4/checksums/sha256-checksums.bin b/.gradle/7.4/checksums/sha256-checksums.bin new file mode 100644 index 0000000..f863312 Binary files /dev/null and b/.gradle/7.4/checksums/sha256-checksums.bin differ diff --git a/.gradle/7.4/checksums/sha512-checksums.bin b/.gradle/7.4/checksums/sha512-checksums.bin new file mode 100644 index 0000000..b8e64d3 Binary files /dev/null and b/.gradle/7.4/checksums/sha512-checksums.bin differ diff --git a/.gradle/7.4/dependencies-accessors/dependencies-accessors.lock b/.gradle/7.4/dependencies-accessors/dependencies-accessors.lock new file mode 100644 index 0000000..3145ad5 Binary files /dev/null and b/.gradle/7.4/dependencies-accessors/dependencies-accessors.lock differ diff --git a/.gradle/7.4/dependencies-accessors/gc.properties b/.gradle/7.4/dependencies-accessors/gc.properties new file mode 100644 index 0000000..e69de29 diff --git a/.gradle/7.4/executionHistory/executionHistory.bin b/.gradle/7.4/executionHistory/executionHistory.bin new file mode 100644 index 0000000..0423119 Binary files /dev/null and b/.gradle/7.4/executionHistory/executionHistory.bin differ diff --git a/.gradle/7.4/executionHistory/executionHistory.lock b/.gradle/7.4/executionHistory/executionHistory.lock new file mode 100644 index 0000000..34b76f5 Binary files /dev/null and b/.gradle/7.4/executionHistory/executionHistory.lock differ diff --git a/.gradle/7.4/fileChanges/last-build.bin b/.gradle/7.4/fileChanges/last-build.bin new file mode 100644 index 0000000..f76dd23 Binary files /dev/null and b/.gradle/7.4/fileChanges/last-build.bin differ diff --git a/.gradle/7.4/fileHashes/fileHashes.bin b/.gradle/7.4/fileHashes/fileHashes.bin new file mode 100644 index 0000000..fd746b7 Binary files /dev/null and b/.gradle/7.4/fileHashes/fileHashes.bin differ diff --git a/.gradle/7.4/fileHashes/fileHashes.lock b/.gradle/7.4/fileHashes/fileHashes.lock new file mode 100644 index 0000000..aae896c Binary files /dev/null and b/.gradle/7.4/fileHashes/fileHashes.lock differ diff --git a/.gradle/7.4/fileHashes/resourceHashesCache.bin b/.gradle/7.4/fileHashes/resourceHashesCache.bin new file mode 100644 index 0000000..a8989be Binary files /dev/null and b/.gradle/7.4/fileHashes/resourceHashesCache.bin differ diff --git a/.gradle/7.4/gc.properties b/.gradle/7.4/gc.properties new file mode 100644 index 0000000..e69de29 diff --git a/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/.gradle/buildOutputCleanup/buildOutputCleanup.lock new file mode 100644 index 0000000..88f16ac Binary files /dev/null and b/.gradle/buildOutputCleanup/buildOutputCleanup.lock differ diff --git a/.gradle/buildOutputCleanup/cache.properties b/.gradle/buildOutputCleanup/cache.properties new file mode 100644 index 0000000..0280e1f --- /dev/null +++ b/.gradle/buildOutputCleanup/cache.properties @@ -0,0 +1,2 @@ +#Sun May 15 20:49:35 CST 2022 +gradle.version=7.4 diff --git a/.gradle/buildOutputCleanup/outputFiles.bin b/.gradle/buildOutputCleanup/outputFiles.bin new file mode 100644 index 0000000..c661684 Binary files /dev/null and b/.gradle/buildOutputCleanup/outputFiles.bin differ diff --git a/.gradle/file-system.probe b/.gradle/file-system.probe new file mode 100644 index 0000000..9692fe5 Binary files /dev/null and b/.gradle/file-system.probe differ diff --git a/.gradle/vcs-1/gc.properties b/.gradle/vcs-1/gc.properties new file mode 100644 index 0000000..e69de29 diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..35410ca --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# 默认忽略的文件 +/shelf/ +/workspace.xml +# 基于编辑器的 HTTP 客户端请求 +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..e1c1988 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +ImageTracer \ No newline at end of file diff --git a/.idea/ImageTracerJava.iml b/.idea/ImageTracerJava.iml new file mode 100644 index 0000000..d6ebd48 --- /dev/null +++ b/.idea/ImageTracerJava.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..61a9130 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/copyright/2_0.xml b/.idea/copyright/2_0.xml new file mode 100644 index 0000000..b25555f --- /dev/null +++ b/.idea/copyright/2_0.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..21cfeff --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/intellij-javadocs-4.0.1.xml b/.idea/intellij-javadocs-4.0.1.xml new file mode 100644 index 0000000..5197c3f --- /dev/null +++ b/.idea/intellij-javadocs-4.0.1.xml @@ -0,0 +1,141 @@ + + + + + UPDATE + false + true + + METHOD + FIELD + TYPE + + + PROTECTED + PUBLIC + DEFAULT + + + + + + ^.*(public|protected|private)*.+interface\s+\w+.* + /**\n + * The interface ${name}.\n +<#if element.typeParameters?has_content> * \n +</#if><#list element.typeParameters as typeParameter> * @param <${typeParameter.name}> the type parameter\n +</#list> */ + + + ^.*(public|protected|private)*.+enum\s+\w+.* + /**\n + * The enum ${name}.\n + */ + + + ^.*(public|protected|private)*.+class\s+\w+.* + /**\n + * The type ${name}.\n +<#if element.typeParameters?has_content> * \n +</#if><#list element.typeParameters as typeParameter> * @param <${typeParameter.name}> the type parameter\n +</#list> */ + + + .+ + /**\n + * The type ${name}.\n + */ + + + + + .+ + /**\n + * Instantiates a new ${name}.\n +<#if element.parameterList.parameters?has_content> *\n +</#if><#list element.parameterList.parameters as parameter> * @param ${parameter.name} the ${paramNames[parameter.name]}\n +</#list><#if element.throwsList.referenceElements?has_content> *\n +</#if><#list element.throwsList.referenceElements as exception> * @throws ${exception.referenceName} the ${exceptionNames[exception.referenceName]}\n +</#list> */ + + + + + ^.*(public|protected|private)*\s*.*(\w(\s*<.+>)*)+\s+get\w+\s*\(.*\).+ + /**\n + * Gets ${partName}.\n +<#if element.typeParameters?has_content> * \n +</#if><#list element.typeParameters as typeParameter> * @param <${typeParameter.name}> the type parameter\n +</#list><#if element.parameterList.parameters?has_content> *\n +</#if><#list element.parameterList.parameters as parameter> * @param ${parameter.name} the ${paramNames[parameter.name]}\n +</#list><#if isNotVoid> *\n + * @return the ${partName}\n +</#if><#if element.throwsList.referenceElements?has_content> *\n +</#if><#list element.throwsList.referenceElements as exception> * @throws ${exception.referenceName} the ${exceptionNames[exception.referenceName]}\n +</#list> */ + + + ^.*(public|protected|private)*\s*.*(void|\w(\s*<.+>)*)+\s+set\w+\s*\(.*\).+ + /**\n + * Sets ${partName}.\n +<#if element.typeParameters?has_content> * \n +</#if><#list element.typeParameters as typeParameter> * @param <${typeParameter.name}> the type parameter\n +</#list><#if element.parameterList.parameters?has_content> *\n +</#if><#list element.parameterList.parameters as parameter> * @param ${parameter.name} the ${paramNames[parameter.name]}\n +</#list><#if isNotVoid> *\n + * @return the ${partName}\n +</#if><#if element.throwsList.referenceElements?has_content> *\n +</#if><#list element.throwsList.referenceElements as exception> * @throws ${exception.referenceName} the ${exceptionNames[exception.referenceName]}\n +</#list> */ + + + ^.*((public\s+static)|(static\s+public))\s+void\s+main\s*\(\s*String\s*(\[\s*\]|\.\.\.)\s+\w+\s*\).+ + /**\n + * The entry point of application.\n + + <#if element.parameterList.parameters?has_content> *\n +</#if> * @param ${element.parameterList.parameters[0].name} the input arguments\n +<#if element.throwsList.referenceElements?has_content> *\n +</#if><#list element.throwsList.referenceElements as exception> * @throws ${exception.referenceName} the ${exceptionNames[exception.referenceName]}\n +</#list> */ + + + .+ + /**\n + * ${name}<#if isNotVoid> ${return}</#if>.\n +<#if element.typeParameters?has_content> * \n +</#if><#list element.typeParameters as typeParameter> * @param <${typeParameter.name}> the type parameter\n +</#list><#if element.parameterList.parameters?has_content> *\n +</#if><#list element.parameterList.parameters as parameter> * @param ${parameter.name} the ${paramNames[parameter.name]}\n +</#list><#if isNotVoid> *\n + * @return the ${return}\n +</#if><#if element.throwsList.referenceElements?has_content> *\n +</#if><#list element.throwsList.referenceElements as exception> * @throws ${exception.referenceName} the ${exceptionNames[exception.referenceName]}\n +</#list> */ + + + + + ^.*(public|protected|private)*.+static.*(\w\s\w)+.+ + /**\n + * The constant ${element.getName()}.\n + */ + + + ^.*(public|protected|private)*.*(\w\s\w)+.+ + /**\n + <#if element.parent.isInterface()> * The constant ${element.getName()}.\n +<#else> * The ${name}.\n +</#if> */ + + + .+ + /**\n + <#if element.parent.isEnum()> *${name} ${typeName}.\n +<#else> * The ${name}.\n +</#if>*/ + + + + + \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 0000000..63346ed --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..6c3a21a --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/sonarlint/issuestore/0/5/05efc8b1657769a27696d478ded1e95f38737233 b/.idea/sonarlint/issuestore/0/5/05efc8b1657769a27696d478ded1e95f38737233 new file mode 100644 index 0000000..e69de29 diff --git a/.idea/sonarlint/issuestore/0/a/0ab6f53a3ca221a03ba699f735c59475a23a261b b/.idea/sonarlint/issuestore/0/a/0ab6f53a3ca221a03ba699f735c59475a23a261b new file mode 100644 index 0000000..df19335 --- /dev/null +++ b/.idea/sonarlint/issuestore/0/a/0ab6f53a3ca221a03ba699f735c59475a23a261b @@ -0,0 +1,34 @@ + +g java:S112:"FDefine and throw a dedicated exception instead of using a generic one.(8֌0 +b java:S112K"FDefine and throw a dedicated exception instead of using a generic one.(8֌0 + +java:S1319q"dThe type of "options" should be an interface such as "Map" rather than the implementation "HashMap".(8֌0 +t +java:S1104"QMake width a static final constant or non-public and provide accessors if needed.(ۋ8֌0 +p +java:S1104"RMake height a static final constant or non-public and provide accessors if needed.(8֌0 +t +java:S1104"QMake width a static final constant or non-public and provide accessors if needed.(ۋ8֌0 +p +java:S1104"RMake height a static final constant or non-public and provide accessors if needed.(8֌0 +s +java:S1104"PMake data a static final constant or non-public and provide accessors if needed.(8֌0 +n +java:S2095?"LUse try-with-resources or close this "BufferedWriter" in a "finally" clause.(8֌0 + +java:S1186/"Add a nested comment explaining why this method is empty, throw an UnsupportedOperationException or complete the implementation.(时8͎֌0 +x +java:S1450*"VRemove the "rawdata" field and declare it as a local variable in the relevant methods.(8ݎ֌0 +? +java:S1118/"Hide this public constructor.(时8쎵֌0 +^ java:S899="BDo something with the "boolean" value returned by "createNewFile".(ܲ8֌0 +P +java:S1172E".Remove this unused method parameter "options".(ڂ8֌0 + +java:S1130K"lRemove the declaration of thrown exception 'java.lang.Exception', as it cannot be thrown from method's body.(8ď֌0 +] java:S125Z" + + + + + \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..fa39823 --- /dev/null +++ b/README.md @@ -0,0 +1,148 @@ +# ImageTracerJava +ImageTracerJava(A library that can transfer pictures such as PNG to SVG)
+ImageTracerJava(一个可以把png等图片转svg的开源Java库) + +## How to Use: + +### Including in Java projects +Add **ImageTracer.jar** to your build path +把 **ImageTracer.jar** 添加到构建路径 +### 安装教程 + +**Add the mavenCentral repository under the project's build.gradle** +**在 Project 的 build.gradle 下添加 mavenCentral 仓库** + +```groovy +repositories { + maven { + url 'https://repo.huaweicloud.com/repository/maven/' + } + maven { + url 'https://developer.huawei.com/repo/' + } + mavenCentral() +} +``` + +**Add ImageTracerJava dependency under Module's build.gradle** +**在 Module 的 build.gradle 下添加 ImageTracerJava 依赖** + +```groovy +implementation 'top.xuegao-tzx:ImageTracerJava:1.1.4.516' +``` + +then use the static methods: +然后使用静态方法: + +```java +import com.xcl.imagetracer_mod.ImageTracer; + +... +ImageTracer.saveString( + "output.svg" , + ImageTracer.imageToSVG("input.jpg",null,null) +); +``` + +With options and palette +带有选项和调色板的方法: + +```java +// Options +HashMap options = new HashMap(); + +// Tracing +options.put("ltres",1f); +options.put("qtres",1f); +options.put("pathomit",8f); + +// Color quantization +options.put("colorsampling",1f); // 1f means true ; 0f means false: starting with generated palette +options.put("numberofcolors",16f); +options.put("mincolorratio",0.02f); +options.put("colorquantcycles",3f); + +// SVG rendering +options.put("scale",1f); +options.put("roundcoords",1f); // 1f means rounded to 1 decimal places, like 7.3 ; 3f means rounded to 3 places, like 7.356 ; etc. +options.put("lcpr",0f); +options.put("qcpr",0f); +options.put("desc",1f); // 1f means true ; 0f means false: SVG descriptions deactivated +options.put("viewbox",0f); // 1f means true ; 0f means false: fixed width and height + +// Selective Gauss Blur +options.put("blurradius",0f); // 0f means deactivated; 1f .. 5f : blur with this radius +options.put("blurdelta",20f); // smaller than this RGB difference will be blurred + +// Palette +// This is an example of a grayscale palette +// please note that signed byte values [ -128 .. 127 ] will be converted to [ 0 .. 255 ] in the getsvgstring function +// the two number '8' below,you can change it to any number between 4 and 16,you need to change this by myself,so that you can make the SVG more clear! +byte[][] palette = new byte[8][4]; +for(int colorcnt=0; colorcnt < 8; colorcnt++){ + palette[colorcnt][0] = (byte)( -128 + colorcnt * 32); // R + palette[colorcnt][1] = (byte)( -128 + colorcnt * 32); // G + palette[colorcnt][2] = (byte)( -128 + colorcnt * 32); // B + palette[colorcnt][3] = (byte)127; // A +} + +ImageTracer.saveString( + "output.svg" , + ImageTracer.imageToSVG("input.jpg",options,palette) +); +``` + +### Deterministic output +See [options for deterministic tracing](https://github.com/jankovicsandras/imagetracerjava/blob/master/deterministic.md) + + +### Main Functions +### 主要功能 +|Function name|Arguments|Returns| +|-------------|---------|-------| +|```imageToSVG```|```String filename, HashMap options /*can be null*/, byte [][] palette /*can be null*/```|```String /*SVG content*/```| +|```imageToSVG```|```BufferedImage image, HashMap options /*can be null*/, byte [][] palette /*can be null*/```|```String /*SVG content*/```| +|```imagedataToSVG```|```ImageData imgd, HashMap options /*can be null*/, byte [][] palette /*can be null*/```|```String /*SVG content*/```| +|```imageToTracedata```|```String filename, HashMap options /*can be null*/, byte [][] palette /*can be null*/```|```IndexedImage /*read the source for details*/```| +|```imageToTracedata```|```BufferedImage image, HashMap options /*can be null*/, byte [][] palette /*can be null*/```|```IndexedImage /*read the source for details*/```| +|```imagedataToTracedata```|```ImageData imgd, HashMap options /*can be null*/, byte [][] palette /*can be null*/```|```IndexedImage /*read the source for details*/```| + + +#### Helper Functions +|Function name|Arguments|Returns| +|-------------|---------|-------| +|```saveString```|```String filename, String str```|```void```| +|```loadImageData```|```String filename```|```ImageData /*read the source for details*/```| +|```loadImageData```|```BufferedImage image```|```ImageData /*read the source for details*/```| + +```ImageData``` is similar to [ImageData](https://developer.mozilla.org/en-US/docs/Web/API/ImageData) here. + +There are more functions for advanced users, read the source if you are interested. :) + +### Options +### 配置选项 +|Option name|Default value|Meaning| +|-----------|-------------|-------| +|```ltres```|```1f```|Error treshold for straight lines.| +|```qtres```|```1f```|Error treshold for quadratic splines.| +|```pathomit```|```8f```|Edge node paths shorter than this will be discarded for noise reduction.| +|```colorsampling```|```1f```|Enable or disable color sampling. 1f is on, 0f is off.| +|```numberofcolors```|```16f```|Number of colors to use on palette if pal object is not defined.| +|```mincolorratio```|```0.02f```|Color quantization will randomize a color if fewer pixels than (total pixels*mincolorratio) has it.| +|```colorquantcycles```|```3f```|Color quantization will be repeated this many times.| +|```blurradius```|```0f```|Set this to 1f..5f for selective Gaussian blur preprocessing.| +|```blurdelta```|```20f```|RGBA delta treshold for selective Gaussian blur preprocessing.| +|```scale```|```1f```|Every coordinate will be multiplied with this, to scale the SVG.| +|```roundcoords```|```1f```|rounding coordinates to a given decimal place. 1f means rounded to 1 decimal place like 7.3 ; 3f means rounded to 3 places, like 7.356| +|```viewbox```|```0f```|Enable or disable SVG viewBox. 1f is on, 0f is off.| +|```desc```|```1f```|Enable or disable SVG descriptions. 1f is on, 0f is off.| +|```lcpr```|```0f```|Straight line control point radius, if this is greater than zero, small circles will be drawn in the SVG. Do not use this for big/complex images.| +|```qcpr```|```0f```|Quadratic spline control point radius, if this is greater than zero, small circles and lines will be drawn in the SVG. Do not use this for big/complex images.| + +### Process overview +See [Process overview and Ideas for improvement](https://github.com/jankovicsandras/imagetracerjava/blob/master/process_overview.md) + +### License +### 许可证 +Modfiy Author:田梓萱
+ImageTracerJava 在 [Apache 2.0 License](LICENSE)下获得许可 \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..25f5428 --- /dev/null +++ b/build.gradle @@ -0,0 +1,40 @@ +apply plugin: 'java' +apply from: './upload.gradle' // 上传完Maven之后需要删除该行代码,否则工程编译会报错 + +group = 'top.xuegao-tzx' +version= '1.1.4.516' + +repositories { + maven { + url 'https://mirrors.huaweicloud.com/repository/maven/' + allowInsecureProtocol = true + } + maven { + url 'https://developer.huawei.com/repo/' + allowInsecureProtocol = true + } + maven { + url 'https://repo.huaweicloud.com/repository/maven/' + allowInsecureProtocol = true + } + mavenCentral() +} + +jar { + enabled = true +} + +javadoc { + options.encoding = "UTF-8" + failOnError = false +} + +dependencies { + implementation 'junit:junit:4.13.1' + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1' +} + +test { + useJUnitPlatform() +} \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..41d9927 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..41dfb87 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..1b6c787 --- /dev/null +++ b/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/out/ImageTracer-1.1.4.516.jar b/out/ImageTracer-1.1.4.516.jar new file mode 100644 index 0000000..c32d604 Binary files /dev/null and b/out/ImageTracer-1.1.4.516.jar differ diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..194a37a --- /dev/null +++ b/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'ImageTracer' + diff --git a/src/main/java/com/xcl/imagetracer_mod/ImageTracer.java b/src/main/java/com/xcl/imagetracer_mod/ImageTracer.java new file mode 100644 index 0000000..f25f2bd --- /dev/null +++ b/src/main/java/com/xcl/imagetracer_mod/ImageTracer.java @@ -0,0 +1,210 @@ +/* + * Copyright 2022 田梓萱, xcl@xuegao-tzx.top + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.xcl.imagetracer_mod; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.util.ArrayList; +import java.util.HashMap; + +//////////////////////////////////////////////////////////////////////////////////////// +// // +// Open source address of the project:https://github.com/xuegao-tzx/ImageTracerJava // +// 本项目开源地址:https://gitee.com/xuegao-tzx/ImageTracerJava // +// 作者:田梓萱(XCL) // +// // +//////////////////////////////////////////////////////////////////////////////////////// +/** + * The type Image tracer. + */ +public class ImageTracer { + + /** + * The Versionnumber. + */ + static String versionnumber = "1.1.4.516"; + private static int[] rawdata; + + /** + * Instantiates a new Image tracer. + */ + public ImageTracer() { + } + + /** + * Save string. + * + * @param filename the filename + * @param str the str + * @throws Exception the exception + */ +// Saving a String as a file + public static void saveString(String filename, String str) throws Exception { + File file = new File(filename); + // if file doesnt exists, then create it + if (!file.exists()) file.createNewFile(); + FileWriter fw = new FileWriter(file.getAbsoluteFile()); + BufferedWriter bw = new BufferedWriter(fw); + bw.write(str); + bw.close(); + } + + // Loading a file to ImageData, ARGB byte order + private static ImageData loadImageData(String filename, HashMap options) throws Exception { + + BufferedImage image = ImageIO.read(new File(filename)); + return ImageTracer.loadImageData(image); + } + + private static ImageData loadImageData(BufferedImage image) throws Exception { + + int width = image.getWidth(); + int height = image.getHeight(); + ImageTracer.rawdata = image.getRGB(0, 0, width, height, null, 0, width); + byte[] data = new byte[ImageTracer.rawdata.length * 4]; + for (int i = 0; i < ImageTracer.rawdata.length; i++) { + data[(i * 4) + 3] = ImageTracer.bytetrans((byte) (ImageTracer.rawdata[i] >>> 24)); + data[i * 4] = ImageTracer.bytetrans((byte) (ImageTracer.rawdata[i] >>> 16)); + data[(i * 4) + 1] = ImageTracer.bytetrans((byte) (ImageTracer.rawdata[i] >>> 8)); + data[(i * 4) + 2] = ImageTracer.bytetrans((byte) (ImageTracer.rawdata[i])); + } + return new ImageData(width, height, data); + } + + // The bitshift method in loadImageData creates signed bytes where -1 -> 255 unsigned ; -128 -> 128 unsigned ; + // 127 -> 127 unsigned ; 0 -> 0 unsigned ; These will be converted to -128 (representing 0 unsigned) ... + // 127 (representing 255 unsigned) and tosvgcolorstr will add +128 to create RGB values 0..255 + private static byte bytetrans(byte b) { + if (b < 0) return (byte) (b + 128); + else return (byte) (b - 128); + } + + /** + * Image to svg string. + * + * @param filename the filename + * @param options the options + * @param palette the palette + * @return the string + * @throws Exception the exception + */ + //////////////////////////////////////////////////////////// + // + // User friendly functions + // + //////////////////////////////////////////////////////////// + // Loading an image from a file, tracing when loaded, then returning the SVG String + public static String imageToSVG(String filename, HashMap options, byte[][] palette) throws Exception { + System.out.println("自定义配置:" + options.toString()); + ImageData imgd = ImageTracer.loadImageData(filename, options); + return ImageTracer.imagedataToSVG(imgd, options, palette); + }// End of imageToSVG() + + // Tracing ImageData, then returning the SVG String + private static String imagedataToSVG(ImageData imgd, HashMap options, byte[][] palette) { + IndexedImage ii = ImageTracer.imagedataToTracedata(imgd, options, palette); + return SVGUtils.getsvgstring(ii, options); + }// End of imagedataToSVG() + + // Tracing ImageData, then returning IndexedImage with tracedata in layers + private static IndexedImage imagedataToTracedata(ImageData imgd, HashMap options, byte[][] palette) { + // 1. Color quantization + IndexedImage ii = VectorizingUtils.colorquantization(imgd, palette, options); + // 2. Layer separation and edge detection + int[][][] rawlayers = VectorizingUtils.layering(ii); + // 3. Batch pathscan + ArrayList>> bps = VectorizingUtils.batchpathscan(rawlayers, (int) (Math.floor(options.get("pathomit")))); + // 4. Batch interpollation + ArrayList>> bis = VectorizingUtils.batchinternodes(bps); + // 5. Batch tracing + ii.layers = VectorizingUtils.batchtracelayers(bis, options.get("ltres"), options.get("qtres")); + return ii; + }// End of imagedataToTracedata() + + /** + * The type Indexed image. + */ +// Container for the color-indexed image before and tracedata after vectorizing + public static class IndexedImage { + /** + * The Width. + */ + public int width, /** + * The Height. + */ + height; + /** + * The Array. + */ + int[][] array; // array[x][y] of palette colors + /** + * The Palette. + */ + byte[][] palette;// array[palettelength][4] RGBA color palette + /** + * The Layers. + */ + ArrayList>> layers;// tracedata + + /** + * Instantiates a new Indexed image. + * + * @param marray the marray + * @param mpalette the mpalette + */ + IndexedImage(int[][] marray, byte[][] mpalette) { + array = marray; + palette = mpalette; + width = marray[0].length - 2; + height = marray.length - 2;// Color quantization adds +2 to the original width and height + } + } + + /** + * The type Image data. + */ +// https://developer.mozilla.org/en-US/docs/Web/API/ImageData + public static class ImageData { + /** + * The Width. + */ + public int width, /** + * The Height. + */ + height; + /** + * The Data. + */ + public byte[] data; // raw byte data: R G B A R G B A ... + + /** + * Instantiates a new Image data. + * + * @param mwidth the mwidth + * @param mheight the mheight + * @param mdata the mdata + */ + ImageData(int mwidth, int mheight, byte[] mdata) { + width = mwidth; + height = mheight; + data = mdata; + } + } + +}// End of ImageTracer class diff --git a/src/main/java/com/xcl/imagetracer_mod/SVGUtils.java b/src/main/java/com/xcl/imagetracer_mod/SVGUtils.java new file mode 100644 index 0000000..e0b0ef9 --- /dev/null +++ b/src/main/java/com/xcl/imagetracer_mod/SVGUtils.java @@ -0,0 +1,151 @@ +/* + * Copyright 2022 田梓萱, xcl@xuegao-tzx.top + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.xcl.imagetracer_mod; + +import com.xcl.imagetracer_mod.ImageTracer.IndexedImage; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map.Entry; +import java.util.TreeMap; + +/** + * The type Svg utils. + */ +class SVGUtils { + + + //////////////////////////////////////////////////////////// + // + // SVG Drawing functions + // + //////////////////////////////////////////////////////////// + + private static float roundtodec(float val, float places) { + return (float) (Math.round(val * Math.pow(10, places)) / Math.pow(10, places)); + } + + + // Getting SVG path element string from a traced path + private static void svgpathstring(StringBuilder sb, String desc, ArrayList segments, String colorstr, HashMap options) { + float scale = options.get("scale"); + float lcpr = options.get("lcpr"); + float qcpr = options.get("qcpr"); + float roundcoords = (float) Math.floor(options.get("roundcoords")); + // Path + sb.append(""); + + // Rendering control points + for (int pcnt = 0; pcnt < segments.size(); pcnt++) { + if ((lcpr > 0) && (segments.get(pcnt)[0] == 1.0)) + sb.append(""); + if ((qcpr > 0) && (segments.get(pcnt)[0] == 2.0)) { + sb.append(""); + sb.append(""); + sb.append(""); + sb.append(""); + }// End of quadratic control points + } + + }// End of svgpathstring() + + + /** + * Gets . + * + * @param ii the ii + * @param options the options + * @return the + */ +// Converting tracedata to an SVG string, paths are drawn according to a Z-index + // the optional lcpr and qcpr are linear and quadratic control point radiuses + static String getsvgstring(IndexedImage ii, HashMap options) { + // SVG start + int w = (int) (ii.width * options.get("scale")); + int h = (int) (ii.height * options.get("scale")); + String viewboxorviewport = options.get("viewbox") != 0 ? "viewBox=\"0 0 " + w + " " + h + "\" " : "width=\"" + w + "\" height=\"" + h + "\" "; + StringBuilder svgstr = new StringBuilder(""); + + // creating Z-index + TreeMap zindex = new TreeMap<>(); + double label; + // Layer loop + // Path loop + // End of layer loop + for (int k = 0; k < ii.layers.size(); k++) + for (int pcnt = 0; pcnt < ii.layers.get(k).size(); pcnt++) { + + // Label (Z-index key) is the startpoint of the path, linearized + label = (ii.layers.get(k).get(pcnt).get(0)[2] * w) + ii.layers.get(k).get(pcnt).get(0)[1]; + // Creating new list if required + if (!zindex.containsKey(label)) zindex.put(label, new Integer[2]); + // Adding layer and path number to list + zindex.get(label)[0] = new Integer(k); + zindex.get(label)[1] = new Integer(pcnt); + }// End of path loop + + // Sorting Z-index is not required, TreeMap is sorted automatically + + // Drawing + // Z-index loop + String thisdesc = ""; + for (Entry entry : zindex.entrySet()) { + if (options.get("desc") != 0) + thisdesc = "desc=\"l " + entry.getValue()[0] + " p " + entry.getValue()[1] + "\" "; + else thisdesc = ""; + SVGUtils.svgpathstring(svgstr, + thisdesc, + ii.layers.get(entry.getValue()[0]).get(entry.getValue()[1]), + SVGUtils.tosvgcolorstr(ii.palette[entry.getValue()[0]]), + options); + } + + // SVG End + svgstr.append(""); + + return svgstr.toString(); + + }// End of getsvgstring() + + + private static String tosvgcolorstr(byte[] c) { + return "fill=\"rgb(" + (c[0] + 128) + "," + (c[1] + 128) + "," + (c[2] + 128) + ")\" stroke=\"rgb(" + (c[0] + 128) + "," + (c[1] + 128) + "," + (c[2] + 128) + ")\" stroke-width=\"1\" opacity=\"" + ((c[3] + 128) / 255.0) + "\" "; + } + + +} diff --git a/src/main/java/com/xcl/imagetracer_mod/SelectiveBlur.java b/src/main/java/com/xcl/imagetracer_mod/SelectiveBlur.java new file mode 100644 index 0000000..b6d805e --- /dev/null +++ b/src/main/java/com/xcl/imagetracer_mod/SelectiveBlur.java @@ -0,0 +1,137 @@ +/* + * Copyright 2022 田梓萱, xcl@xuegao-tzx.top + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.xcl.imagetracer_mod; + +import com.xcl.imagetracer_mod.ImageTracer.ImageData; + +/** + * The type Selective blur. + */ +class SelectiveBlur { + + + // Gaussian kernels for blur + private static final double[][] gks = {{0.27901, 0.44198, 0.27901}, {0.135336, 0.228569, 0.272192, 0.228569, 0.135336}, {0.086776, 0.136394, 0.178908, 0.195843, 0.178908, 0.136394, 0.086776}, + {0.063327, 0.093095, 0.122589, 0.144599, 0.152781, 0.144599, 0.122589, 0.093095, 0.063327}, {0.049692, 0.069304, 0.089767, 0.107988, 0.120651, 0.125194, 0.120651, 0.107988, 0.089767, 0.069304, 0.049692}}; + + + /** + * Blur image data. + * + * @param imgd the imgd + * @param rad the rad + * @param del the del + * @return the image data + */ +// Selective Gaussian blur for preprocessing + static ImageData blur(ImageData imgd, float rad, float del) { + int i, j, k, d, idx; + double racc, gacc, bacc, aacc, wacc; + ImageData imgd2 = new ImageData(imgd.width, imgd.height, new byte[imgd.width * imgd.height * 4]); + + // radius and delta limits, this kernel + int radius = (int) Math.floor(rad); + if (radius < 1) return imgd; + if (radius > 5) radius = 5; + int delta = (int) Math.abs(del); + if (delta > 1024) delta = 1024; + double[] thisgk = SelectiveBlur.gks[radius - 1]; + + // loop through all pixels, horizontal blur + // End of horizontal blur + for (j = 0; j < imgd.height; j++) + for (i = 0; i < imgd.width; i++) { + + racc = 0; + gacc = 0; + bacc = 0; + aacc = 0; + wacc = 0; + // gauss kernel loop + // add weighted color values + for (k = -radius; k < (radius + 1); k++) + if (((i + k) > 0) && ((i + k) < imgd.width)) { + idx = ((j * imgd.width) + i + k) * 4; + racc += imgd.data[idx] * thisgk[k + radius]; + gacc += imgd.data[idx + 1] * thisgk[k + radius]; + bacc += imgd.data[idx + 2] * thisgk[k + radius]; + aacc += imgd.data[idx + 3] * thisgk[k + radius]; + wacc += thisgk[k + radius]; + } + // The new pixel + idx = ((j * imgd.width) + i) * 4; + imgd2.data[idx] = (byte) Math.floor(racc / wacc); + imgd2.data[idx + 1] = (byte) Math.floor(gacc / wacc); + imgd2.data[idx + 2] = (byte) Math.floor(bacc / wacc); + imgd2.data[idx + 3] = (byte) Math.floor(aacc / wacc); + + }// End of width loop + + // copying the half blurred imgd2 + byte[] himgd = imgd2.data.clone(); + + // loop through all pixels, vertical blur + // End of vertical blur + for (j = 0; j < imgd.height; j++) + for (i = 0; i < imgd.width; i++) { + + racc = 0; + gacc = 0; + bacc = 0; + aacc = 0; + wacc = 0; + // gauss kernel loop + // add weighted color values + for (k = -radius; k < (radius + 1); k++) + if (((j + k) > 0) && ((j + k) < imgd.height)) { + idx = (((j + k) * imgd.width) + i) * 4; + racc += himgd[idx] * thisgk[k + radius]; + gacc += himgd[idx + 1] * thisgk[k + radius]; + bacc += himgd[idx + 2] * thisgk[k + radius]; + aacc += himgd[idx + 3] * thisgk[k + radius]; + wacc += thisgk[k + radius]; + } + // The new pixel + idx = ((j * imgd.width) + i) * 4; + imgd2.data[idx] = (byte) Math.floor(racc / wacc); + imgd2.data[idx + 1] = (byte) Math.floor(gacc / wacc); + imgd2.data[idx + 2] = (byte) Math.floor(bacc / wacc); + imgd2.data[idx + 3] = (byte) Math.floor(aacc / wacc); + + }// End of width loop + + // Selective blur: loop through all pixels + // End of Selective blur + for (j = 0; j < imgd.height; j++) + for (i = 0; i < imgd.width; i++) { + + idx = ((j * imgd.width) + i) * 4; + // d is the difference between the blurred and the original pixel + d = Math.abs(imgd2.data[idx] - imgd.data[idx]) + Math.abs(imgd2.data[idx + 1] - imgd.data[idx + 1]) + + Math.abs(imgd2.data[idx + 2] - imgd.data[idx + 2]) + Math.abs(imgd2.data[idx + 3] - imgd.data[idx + 3]); + // selective blur: if d>delta, put the original pixel back + if (d > delta) { + imgd2.data[idx] = imgd.data[idx]; + imgd2.data[idx + 1] = imgd.data[idx + 1]; + imgd2.data[idx + 2] = imgd.data[idx + 2]; + imgd2.data[idx + 3] = imgd.data[idx + 3]; + } + } + + return imgd2; + + }// End of blur() +} diff --git a/src/main/java/com/xcl/imagetracer_mod/VectorizingUtils.java b/src/main/java/com/xcl/imagetracer_mod/VectorizingUtils.java new file mode 100644 index 0000000..de8abb9 --- /dev/null +++ b/src/main/java/com/xcl/imagetracer_mod/VectorizingUtils.java @@ -0,0 +1,567 @@ +/* + * Copyright 2022 田梓萱, xcl@xuegao-tzx.top + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.xcl.imagetracer_mod; + +import com.xcl.imagetracer_mod.ImageTracer.ImageData; +import com.xcl.imagetracer_mod.ImageTracer.IndexedImage; + +import java.util.ArrayList; +import java.util.HashMap; + +/** + * The type Vectorizing utils. + */ +class VectorizingUtils { + + + //////////////////////////////////////////////////////////// + // + // Vectorizing functions + // + //////////////////////////////////////////////////////////// + + // Lookup tables for pathscan + private static final byte[] pathscan_dir_lookup = {0, 0, 3, 0, 1, 0, 3, 0, 0, 3, 3, 1, 0, 3, 0, 0}; + private static final boolean[] pathscan_holepath_lookup = {false, false, false, false, false, false, false, true, false, false, false, true, false, true, true, false}; + // pathscan_combined_lookup[ arr[py][px] ][ dir ] = [nextarrpypx, nextdir, deltapx, deltapy]; + private static final byte[][][] pathscan_combined_lookup = { + {{-1, -1, -1, -1}, {-1, -1, -1, -1}, {-1, -1, -1, -1}, {-1, -1, -1, -1}},// arr[py][px]==0 is invalid + {{0, 1, 0, -1}, {-1, -1, -1, -1}, {-1, -1, -1, -1}, {0, 2, -1, 0}}, + {{-1, -1, -1, -1}, {-1, -1, -1, -1}, {0, 1, 0, -1}, {0, 0, 1, 0}}, + {{0, 0, 1, 0}, {-1, -1, -1, -1}, {0, 2, -1, 0}, {-1, -1, -1, -1}}, + + {{-1, -1, -1, -1}, {0, 0, 1, 0}, {0, 3, 0, 1}, {-1, -1, -1, -1}}, + {{13, 3, 0, 1}, {13, 2, -1, 0}, {7, 1, 0, -1}, {7, 0, 1, 0}}, + {{-1, -1, -1, -1}, {0, 1, 0, -1}, {-1, -1, -1, -1}, {0, 3, 0, 1}}, + {{0, 3, 0, 1}, {0, 2, -1, 0}, {-1, -1, -1, -1}, {-1, -1, -1, -1}}, + + {{0, 3, 0, 1}, {0, 2, -1, 0}, {-1, -1, -1, -1}, {-1, -1, -1, -1}}, + {{-1, -1, -1, -1}, {0, 1, 0, -1}, {-1, -1, -1, -1}, {0, 3, 0, 1}}, + {{11, 1, 0, -1}, {14, 0, 1, 0}, {14, 3, 0, 1}, {11, 2, -1, 0}}, + {{-1, -1, -1, -1}, {0, 0, 1, 0}, {0, 3, 0, 1}, {-1, -1, -1, -1}}, + + {{0, 0, 1, 0}, {-1, -1, -1, -1}, {0, 2, -1, 0}, {-1, -1, -1, -1}}, + {{-1, -1, -1, -1}, {-1, -1, -1, -1}, {0, 1, 0, -1}, {0, 0, 1, 0}}, + {{0, 1, 0, -1}, {-1, -1, -1, -1}, {-1, -1, -1, -1}, {0, 2, -1, 0}}, + {{-1, -1, -1, -1}, {-1, -1, -1, -1}, {-1, -1, -1, -1}, {-1, -1, -1, -1}}// arr[py][px]==15 is invalid + }; + + /** + * Colorquantization indexed image. + * + * @param imgd the imgd + * @param palette the palette + * @param options the options + * @return the indexed image + */ +// 1. Color quantization repeated "cycles" times, based on K-means clustering + // https://en.wikipedia.org/wiki/Color_quantization https://en.wikipedia.org/wiki/K-means_clustering + static IndexedImage colorquantization(ImageData imgd, byte[][] palette, HashMap options) { + + // Selective Gaussian blur preprocessing + if (options.get("blurradius") > 0) + imgd = SelectiveBlur.blur(imgd, options.get("blurradius"), options.get("blurdelta")); + + int cycles = (int) Math.floor(options.get("colorquantcycles")); + // Creating indexed color array arr which has a boundary filled with -1 in every direction + int[][] arr = new int[imgd.height + 2][imgd.width + 2]; + for (int j = 0; j < (imgd.height + 2); j++) { + arr[j][0] = -1; + arr[j][imgd.width + 1] = -1; + } + for (int i = 0; i < (imgd.width + 2); i++) { + arr[0][i] = -1; + arr[imgd.height + 1][i] = -1; + } + + int idx = 0, cd, cdl, ci, c1, c2, c3, c4; + + + byte[][] original_palette_backup = palette; + long[][] paletteacc = new long[palette.length][5]; + + // Repeat clustering step "cycles" times + for (int cnt = 0; cnt < cycles; cnt++) { + + // Average colors from the second iteration + // averaging paletteacc for palette + //float ratio; + // averaging + //ratio = (float)( (double)(paletteacc[k][4]) / (double)(imgd.width*imgd.height) ); + /*// Randomizing a color, if there are too few pixels and there will be a new cycle + if( (ratio 0) for (int k = 0; k < palette.length; k++) + if (paletteacc[k][3] > 0) { + palette[k][0] = (byte) (-128 + (paletteacc[k][0] / paletteacc[k][4])); + palette[k][1] = (byte) (-128 + (paletteacc[k][1] / paletteacc[k][4])); + palette[k][2] = (byte) (-128 + (paletteacc[k][2] / paletteacc[k][4])); + palette[k][3] = (byte) (-128 + (paletteacc[k][3] / paletteacc[k][4])); + } + + // Reseting palette accumulator for averaging + for (int i = 0; i < palette.length; i++) { + paletteacc[i][0] = 0; + paletteacc[i][1] = 0; + paletteacc[i][2] = 0; + paletteacc[i][3] = 0; + paletteacc[i][4] = 0; + } + + // loop through all pixels + // End of j loop + for (int j = 0; j < imgd.height; j++) + for (int i = 0; i < imgd.width; i++) { + + idx = ((j * imgd.width) + i) * 4; + + // find closest color from original_palette_backup by measuring (rectilinear) + // color distance between this pixel and all palette colors + cdl = 256 + 256 + 256 + 256; + ci = 0; + for (int k = 0; k < original_palette_backup.length; k++) { + + // In my experience, https://en.wikipedia.org/wiki/Rectilinear_distance works better than https://en.wikipedia.org/wiki/Euclidean_distance + c1 = Math.abs(original_palette_backup[k][0] - imgd.data[idx]); + c2 = Math.abs(original_palette_backup[k][1] - imgd.data[idx + 1]); + c3 = Math.abs(original_palette_backup[k][2] - imgd.data[idx + 2]); + c4 = Math.abs(original_palette_backup[k][3] - imgd.data[idx + 3]); + cd = c1 + c2 + c3 + (c4 * 4); // weighted alpha seems to help images with transparency + + // Remember this color if this is the closest yet + if (cd < cdl) { + cdl = cd; + ci = k; + } + + }// End of palette loop + + // add to palettacc + paletteacc[ci][0] += 128 + imgd.data[idx]; + paletteacc[ci][1] += 128 + imgd.data[idx + 1]; + paletteacc[ci][2] += 128 + imgd.data[idx + 2]; + paletteacc[ci][3] += 128 + imgd.data[idx + 3]; + paletteacc[ci][4]++; + + arr[j + 1][i + 1] = ci; + }// End of i loop + + }// End of Repeat clustering step "cycles" times + + return new IndexedImage(arr, original_palette_backup); + }// End of colorquantization + + /** + * Layering int [ ] [ ] [ ]. + * + * @param ii the ii + * @return the int [ ] [ ] [ ] + */ +// 2. Layer separation and edge detection + // Edge node types ( ▓:light or 1; ░:dark or 0 ) + // 12 ░░ ▓░ ░▓ ▓▓ ░░ ▓░ ░▓ ▓▓ ░░ ▓░ ░▓ ▓▓ ░░ ▓░ ░▓ ▓▓ + // 48 ░░ ░░ ░░ ░░ ░▓ ░▓ ░▓ ░▓ ▓░ ▓░ ▓░ ▓░ ▓▓ ▓▓ ▓▓ ▓▓ + // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 + // + static int[][][] layering(IndexedImage ii) { + // Creating layers for each indexed color in arr + int val = 0; + int aw = ii.array[0].length; + int ah = ii.array.length; + int n1; + int n2; + int n3; + int n4; + int n5; + int n6; + int n7; + int n8; + + int[][][] layers = new int[ii.palette.length][ah][aw]; + + // Looping through all pixels and calculating edge node type + // End of j loop + for (int j = 1; j < (ah - 1); j++) + for (int i = 1; i < (aw - 1); i++) { + + // This pixel's indexed color + val = ii.array[j][i]; + + // Are neighbor pixel colors the same? + n1 = ii.array[j - 1][i - 1] == val ? 1 : 0; + n2 = ii.array[j - 1][i] == val ? 1 : 0; + n3 = ii.array[j - 1][i + 1] == val ? 1 : 0; + n4 = ii.array[j][i - 1] == val ? 1 : 0; + n5 = ii.array[j][i + 1] == val ? 1 : 0; + n6 = ii.array[j + 1][i - 1] == val ? 1 : 0; + n7 = ii.array[j + 1][i] == val ? 1 : 0; + n8 = ii.array[j + 1][i + 1] == val ? 1 : 0; + + // this pixel"s type and looking back on previous pixels + layers[val][j + 1][i + 1] = 1 + (n5 * 2) + (n8 * 4) + (n7 * 8); + if (n4 == 0) layers[val][j + 1][i] = 0 + 2 + (n7 * 4) + (n6 * 8); + if (n2 == 0) layers[val][j][i + 1] = 0 + (n3 * 2) + (n5 * 4) + 8; + if (n1 == 0) layers[val][j][i] = 0 + (n2 * 2) + 4 + (n4 * 8); + + }// End of i loop + + return layers; + }// End of layering() + + // 3. Walking through an edge node array, discarding edge node types 0 and 15 and creating paths from the rest. + // Walk directions (dir): 0 > ; 1 ^ ; 2 < ; 3 v + // Edge node types ( ▓:light or 1; ░:dark or 0 ) + // ░░ ▓░ ░▓ ▓▓ ░░ ▓░ ░▓ ▓▓ ░░ ▓░ ░▓ ▓▓ ░░ ▓░ ░▓ ▓▓ + // ░░ ░░ ░░ ░░ ░▓ ░▓ ░▓ ░▓ ▓░ ▓░ ▓░ ▓░ ▓▓ ▓▓ ▓▓ ▓▓ + // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 + // + private static ArrayList> pathscan(int[][] arr, float pathomit) { + ArrayList> paths = new ArrayList<>(); + ArrayList thispath; + int px = 0; + int py = 0; + int w = arr[0].length; + int h = arr.length; + int dir = 0; + boolean pathfinished = true, holepath = false; + byte[] lookuprow; + + // End of i loop + // End of j loop + for (int j = 0; j < h; j++) + for (int i = 0; i < w; i++) + if ((arr[j][i] != 0) && (arr[j][i] != 15)) { + + // Init + px = i; + py = j; + paths.add(new ArrayList<>()); + thispath = paths.get(paths.size() - 1); + pathfinished = false; + + // fill paths will be drawn, but hole paths are also required to remove unnecessary edge nodes + dir = VectorizingUtils.pathscan_dir_lookup[arr[py][px]]; + holepath = VectorizingUtils.pathscan_holepath_lookup[arr[py][px]]; + + // Path points loop + while (!pathfinished) { + + // New path point + thispath.add(new Integer[3]); + thispath.get(thispath.size() - 1)[0] = px - 1; + thispath.get(thispath.size() - 1)[1] = py - 1; + thispath.get(thispath.size() - 1)[2] = arr[py][px]; + + // Next: look up the replacement, direction and coordinate changes = clear this cell, turn if required, walk forward + lookuprow = VectorizingUtils.pathscan_combined_lookup[arr[py][px]][dir]; + arr[py][px] = lookuprow[0]; + dir = lookuprow[1]; + px += lookuprow[2]; + py += lookuprow[3]; + + // Close path + if (((px - 1) == thispath.get(0)[0]) && ((py - 1) == thispath.get(0)[1])) { + pathfinished = true; + // Discarding 'hole' type paths and paths shorter than pathomit + if ((holepath) || (thispath.size() < pathomit)) paths.remove(thispath); + } + + }// End of Path points loop + + }// End of Follow path + + return paths; + }// End of pathscan() + + + /** + * Batchpathscan array list. + * + * @param layers the layers + * @param pathomit the pathomit + * @return the array list + */ +// 3. Batch pathscan + static ArrayList>> batchpathscan(int[][][] layers, float pathomit) { + ArrayList>> bpaths = new ArrayList<>(); + for (int[][] layer : layers) bpaths.add(VectorizingUtils.pathscan(layer, pathomit)); + return bpaths; + } + + + // 4. interpolating between path points for nodes with 8 directions ( East, SouthEast, S, SW, W, NW, N, NE ) + private static ArrayList> internodes(ArrayList> paths) { + ArrayList> ins = new ArrayList<>(); + ArrayList thisinp; + Double[] thispoint; + Double[] nextpoint = new Double[2]; + Integer[] pp1, pp2, pp3; + int palen = 0, nextidx = 0, nextidx2 = 0; + + // paths loop + for (int pacnt = 0; pacnt < paths.size(); pacnt++) { + ins.add(new ArrayList<>()); + thisinp = ins.get(ins.size() - 1); + palen = paths.get(pacnt).size(); + // pathpoints loop + for (int pcnt = 0; pcnt < palen; pcnt++) { + + // interpolate between two path points + nextidx = (pcnt + 1) % palen; + nextidx2 = (pcnt + 2) % palen; + thisinp.add(new Double[3]); + thispoint = thisinp.get(thisinp.size() - 1); + pp1 = paths.get(pacnt).get(pcnt); + pp2 = paths.get(pacnt).get(nextidx); + pp3 = paths.get(pacnt).get(nextidx2); + thispoint[0] = (pp1[0] + pp2[0]) / 2.0; + thispoint[1] = (pp1[1] + pp2[1]) / 2.0; + nextpoint[0] = (pp2[0] + pp3[0]) / 2.0; + nextpoint[1] = (pp2[1] + pp3[1]) / 2.0; + + // line segment direction to the next point + // SouthEast + if (thispoint[0] < nextpoint[0]) if (thispoint[1] < nextpoint[1]) thispoint[2] = 1.0; + else // NE + // E + if (thispoint[1] > nextpoint[1]) thispoint[2] = 7.0; + else thispoint[2] = 0.0; + else // SW + // S + if (thispoint[0] > nextpoint[0]) if (thispoint[1] < nextpoint[1]) thispoint[2] = 3.0; + else // NW + // W + if (thispoint[1] > nextpoint[1]) thispoint[2] = 5.0; + else thispoint[2] = 4.0; + else if (thispoint[1] < nextpoint[1]) thispoint[2] = 2.0; + else // N + // center, this should not happen + if (thispoint[1] > nextpoint[1]) thispoint[2] = 6.0; + else thispoint[2] = 8.0; + + }// End of pathpoints loop + }// End of paths loop + return ins; + }// End of internodes() + + + /** + * Batchinternodes array list. + * + * @param bpaths the bpaths + * @return the array list + */ +// 4. Batch interpollation + static ArrayList>> batchinternodes(ArrayList>> bpaths) { + ArrayList>> binternodes = new ArrayList<>(); + for (int k = 0; k < bpaths.size(); k++) binternodes.add(VectorizingUtils.internodes(bpaths.get(k))); + return binternodes; + } + + + // 5. tracepath() : recursively trying to fit straight and quadratic spline segments on the 8 direction internode path + + // 5.1. Find sequences of points with only 2 segment types + // 5.2. Fit a straight line on the sequence + // 5.3. If the straight line fails (an error>ltreshold), find the point with the biggest error + // 5.4. Fit a quadratic spline through errorpoint (project this to get controlpoint), then measure errors on every point in the sequence + // 5.5. If the spline fails (an error>qtreshold), find the point with the biggest error, set splitpoint = (fitting point + errorpoint)/2 + // 5.6. Split sequence and recursively apply 5.2. - 5.7. to startpoint-splitpoint and splitpoint-endpoint sequences + // 5.7. TODO? If splitpoint-endpoint is a spline, try to add new points from the next sequence + + // This returns an SVG Path segment as a double[7] where + // segment[0] ==1.0 linear ==2.0 quadratic interpolation + // segment[1] , segment[2] : x1 , y1 + // segment[3] , segment[4] : x2 , y2 ; middle point of Q curve, endpoint of L line + // segment[5] , segment[6] : x3 , y3 for Q curve, should be 0.0 , 0.0 for L line + // + // path type is discarded, no check for path.size < 3 , which should not happen + + private static ArrayList tracepath(ArrayList path, float ltreshold, float qtreshold) { + int pcnt = 0, seqend = 0; + double segtype1, segtype2; + ArrayList smp = new ArrayList<>(); + //Double [] thissegment; + int pathlength = path.size(); + + while (pcnt < pathlength) { + // 5.1. Find sequences of points with only 2 segment types + segtype1 = path.get(pcnt)[2]; + segtype2 = -1; + seqend = pcnt + 1; + while ( + ((path.get(seqend)[2] == segtype1) || (path.get(seqend)[2] == segtype2) || (segtype2 == -1)) + && (seqend < (pathlength - 1))) { + if ((path.get(seqend)[2] != segtype1) && (segtype2 == -1)) segtype2 = path.get(seqend)[2]; + seqend++; + } + if (seqend == (pathlength - 1)) seqend = 0; + + // 5.2. - 5.6. Split sequence and recursively apply 5.2. - 5.6. to startpoint-splitpoint and splitpoint-endpoint sequences + smp.addAll(VectorizingUtils.fitseq(path, ltreshold, qtreshold, pcnt, seqend)); + // 5.7. TODO? If splitpoint-endpoint is a spline, try to add new points from the next sequence + + // forward pcnt; + if (seqend > 0) pcnt = seqend; + else pcnt = pathlength; + + }// End of pcnt loop + + return smp; + + }// End of tracepath() + + + // 5.2. - 5.6. recursively fitting a straight or quadratic line segment on this sequence of path nodes, + // called from tracepath() + private static ArrayList fitseq(ArrayList path, float ltreshold, float qtreshold, int seqstart, int seqend) { + ArrayList segment = new ArrayList<>(); + Double[] thissegment; + int pathlength = path.size(); + + // return if invalid seqend + if ((seqend > pathlength) || (seqend < 0)) return segment; + + int errorpoint = seqstart; + boolean curvepass = true; + double px, py, dist2, errorval = 0; + double tl = (seqend - seqstart); + if (tl < 0) tl += pathlength; + double vx = (path.get(seqend)[0] - path.get(seqstart)[0]) / tl; + double vy = (path.get(seqend)[1] - path.get(seqstart)[1]) / tl; + + // 5.2. Fit a straight line on the sequence + int pcnt = (seqstart + 1) % pathlength; + double pl; + while (pcnt != seqend) { + pl = pcnt - seqstart; + if (pl < 0) pl += pathlength; + px = path.get(seqstart)[0] + (vx * pl); + py = path.get(seqstart)[1] + (vy * pl); + dist2 = ((path.get(pcnt)[0] - px) * (path.get(pcnt)[0] - px)) + ((path.get(pcnt)[1] - py) * (path.get(pcnt)[1] - py)); + if (dist2 > ltreshold) curvepass = false; + if (dist2 > errorval) { + errorpoint = pcnt; + errorval = dist2; + } + pcnt = (pcnt + 1) % pathlength; + } + + // return straight line if fits + if (curvepass) { + segment.add(new Double[7]); + thissegment = segment.get(segment.size() - 1); + thissegment[0] = 1.0; + thissegment[1] = path.get(seqstart)[0]; + thissegment[2] = path.get(seqstart)[1]; + thissegment[3] = path.get(seqend)[0]; + thissegment[4] = path.get(seqend)[1]; + thissegment[5] = 0.0; + thissegment[6] = 0.0; + return segment; + } + + // 5.3. If the straight line fails (an error>ltreshold), find the point with the biggest error + int fitpoint = errorpoint; + curvepass = true; + errorval = 0; + + // 5.4. Fit a quadratic spline through this point, measure errors on every point in the sequence + // helpers and projecting to get control point + double t = (fitpoint - seqstart) / tl, t1 = (1.0 - t) * (1.0 - t), t2 = 2.0 * (1.0 - t) * t, t3 = t * t; + double cpx = (((t1 * path.get(seqstart)[0]) + (t3 * path.get(seqend)[0])) - path.get(fitpoint)[0]) / -t2; + double cpy = (((t1 * path.get(seqstart)[1]) + (t3 * path.get(seqend)[1])) - path.get(fitpoint)[1]) / -t2; + + // Check every point + pcnt = seqstart + 1; + while (pcnt != seqend) { + + t = (pcnt - seqstart) / tl; + t1 = (1.0 - t) * (1.0 - t); + t2 = 2.0 * (1.0 - t) * t; + t3 = t * t; + px = (t1 * path.get(seqstart)[0]) + (t2 * cpx) + (t3 * path.get(seqend)[0]); + py = (t1 * path.get(seqstart)[1]) + (t2 * cpy) + (t3 * path.get(seqend)[1]); + + dist2 = ((path.get(pcnt)[0] - px) * (path.get(pcnt)[0] - px)) + ((path.get(pcnt)[1] - py) * (path.get(pcnt)[1] - py)); + + if (dist2 > qtreshold) curvepass = false; + if (dist2 > errorval) { + errorpoint = pcnt; + errorval = dist2; + } + pcnt = (pcnt + 1) % pathlength; + } + + // return spline if fits + if (curvepass) { + segment.add(new Double[7]); + thissegment = segment.get(segment.size() - 1); + thissegment[0] = 2.0; + thissegment[1] = path.get(seqstart)[0]; + thissegment[2] = path.get(seqstart)[1]; + thissegment[3] = cpx; + thissegment[4] = cpy; + thissegment[5] = path.get(seqend)[0]; + thissegment[6] = path.get(seqend)[1]; + return segment; + } + + // 5.5. If the spline fails (an error>qtreshold), find the point with the biggest error, + // set splitpoint = (fitting point + errorpoint)/2 + int splitpoint = (fitpoint + errorpoint) / 2; + + // 5.6. Split sequence and recursively apply 5.2. - 5.6. to startpoint-splitpoint and splitpoint-endpoint sequences + segment = VectorizingUtils.fitseq(path, ltreshold, qtreshold, seqstart, splitpoint); + segment.addAll(VectorizingUtils.fitseq(path, ltreshold, qtreshold, splitpoint, seqend)); + return segment; + + }// End of fitseq() + + + // 5. Batch tracing paths + private static ArrayList> batchtracepaths(ArrayList> internodepaths, float ltres, float qtres) { + ArrayList> btracedpaths = new ArrayList<>(); + for (int k = 0; k < internodepaths.size(); k++) + btracedpaths.add(VectorizingUtils.tracepath(internodepaths.get(k), ltres, qtres)); + return btracedpaths; + } + + + /** + * Batchtracelayers array list. + * + * @param binternodes the binternodes + * @param ltres the ltres + * @param qtres the qtres + * @return the array list + */ +// 5. Batch tracing layers + static ArrayList>> batchtracelayers(ArrayList>> binternodes, float ltres, float qtres) { + ArrayList>> btbis = new ArrayList<>(); + for (int k = 0; k < binternodes.size(); k++) + btbis.add(VectorizingUtils.batchtracepaths(binternodes.get(k), ltres, qtres)); + return btbis; + } + + +} diff --git a/src/test/java/com/xcl/imagetracer_mod/Test.java b/src/test/java/com/xcl/imagetracer_mod/Test.java new file mode 100644 index 0000000..580fdba --- /dev/null +++ b/src/test/java/com/xcl/imagetracer_mod/Test.java @@ -0,0 +1,65 @@ +/* + * Copyright 2022 田梓萱, xcl@xuegao-tzx.top + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.xcl.imagetracer_mod; + +import java.util.HashMap; + +/** + * @author Xcl + * @date 2022/5/15 + * @package com.xcl.imagetracer_mod + */ +public class Test { + @org.junit.Test + public void colorquantization() throws Exception { + // Options + HashMap options = new HashMap<>(); +// Tracing + options.put("ltres", 1f); + options.put("qtres", 1f); + options.put("pathomit", 8f); +// Color quantization + options.put("colorsampling", 1f); // 1f means true ; 0f means false: starting with generated palette + options.put("numberofcolors", 16f); + options.put("mincolorratio", 0.02f); + options.put("colorquantcycles", 3f); +// SVG rendering + options.put("scale", 1f); + options.put("roundcoords", 1f); // 1f means rounded to 1 decimal places, like 7.3 ; 3f means rounded to 3 places, like 7.356 ; etc. + options.put("lcpr", 0f); + options.put("qcpr", 0f); + options.put("desc", 0f); // 1f means true ; 0f means false: SVG descriptions deactivated + options.put("viewbox", 0f); // 1f means true ; 0f means false: fixed width and height +// Selective Gauss Blur + options.put("blurradius", 0f); // 0f means deactivated; 1f .. 5f : blur with this radius + options.put("blurdelta", 20f); // smaller than this RGB difference will be blurred +// Palette +// This is an example of a grayscale palette +// please note that signed byte values [ -128 .. 127 ] will be converted to [ 0 .. 255 ] in the getsvgstring function + //下方的16建议根据具体图形自定义 + byte[][] palette = new byte[16][4]; + for (int colorcnt = 0; colorcnt < 16; colorcnt++) { + palette[colorcnt][0] = (byte) (-128 + colorcnt * 32); // R + palette[colorcnt][1] = (byte) (-128 + colorcnt * 32); // G + palette[colorcnt][2] = (byte) (-128 + colorcnt * 32); // B + palette[colorcnt][3] = (byte) 127; // A + } + ImageTracer.saveString( + "src\\test\\resources\\media\\panda.svg", + ImageTracer.imageToSVG("src\\test\\resources\\media\\panda.png", options, palette) + ); + } +} diff --git a/src/test/resources/media/panda.png b/src/test/resources/media/panda.png new file mode 100644 index 0000000..c3e6b52 Binary files /dev/null and b/src/test/resources/media/panda.png differ diff --git a/src/test/resources/media/panda.svg b/src/test/resources/media/panda.svg new file mode 100644 index 0000000..5096f1e --- /dev/null +++ b/src/test/resources/media/panda.svg @@ -0,0 +1,1002 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file