All Downloads are FREE. Search and download functionalities are using the official Maven repository.
Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
com.jzbrooks.vgo.Application.kt Maven / Gradle / Ivy
package com.jzbrooks.vgo
import com.android.ide.common.vectordrawable.Svg2Vector
import com.jzbrooks.BuildConstants
import com.jzbrooks.vgo.core.Writer
import com.jzbrooks.vgo.svg.ScalableVectorGraphic
import com.jzbrooks.vgo.svg.ScalableVectorGraphicWriter
import com.jzbrooks.vgo.svg.SvgOptimizationRegistry
import com.jzbrooks.vgo.svg.parse
import com.jzbrooks.vgo.util.xml.asSequence
import com.jzbrooks.vgo.vd.VectorDrawable
import com.jzbrooks.vgo.vd.VectorDrawableOptimizationRegistry
import com.jzbrooks.vgo.vd.VectorDrawableWriter
import com.jzbrooks.vgo.vd.toSvg
import org.w3c.dom.Document
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.File
import javax.xml.parsers.DocumentBuilderFactory
import kotlin.math.absoluteValue
import kotlin.math.roundToInt
import kotlin.system.exitProcess
class Application {
private var printStats = false
private var printFileNames = false
private var totalBytesBefore = 0.0
private var totalBytesAfter = 0.0
private var outputFormat: String? = null
fun run(args: Array): Int {
val argReader = ArgReader(args.toMutableList())
if (argReader.readFlag("help|h")) {
println(HELP_MESSAGE)
return 0
}
if (argReader.readFlag("version|v")) {
println(BuildConstants.VERSION_NAME)
return 0
}
val writerOptions = mutableSetOf()
argReader.readOption("indent")?.toIntOrNull()?.let { indentColumns ->
writerOptions.add(Writer.Option.Indent(indentColumns))
}
printStats = argReader.readFlag("stats|s")
val outputs =
run {
val outputPaths = mutableListOf()
var output = argReader.readOption("output|o")
while (output != null) {
outputPaths.add(output)
output = argReader.readOption("output|o")
}
outputPaths.toList()
}
outputFormat = argReader.readOption("format")
var inputs = argReader.readArguments()
if (inputs.isEmpty()) {
require(outputs.isEmpty())
var path = readlnOrNull()
val standardInPaths = mutableListOf()
while (path != null) {
standardInPaths.add(path)
path = readlnOrNull()
}
inputs = standardInPaths
}
val inputOutputMap =
if (outputs.isNotEmpty()) {
inputs.zip(outputs) { a, b ->
Pair(File(a), File(b))
}
} else {
inputs.zip(inputs) { a, b ->
Pair(File(a), File(b))
}
}.toMap()
val files = inputOutputMap.count { (input, _) -> input.isFile }
val containsDirectory = inputOutputMap.any { (input, _) -> input.isDirectory }
printFileNames = printStats && (files > 1 || containsDirectory)
return handleFiles(inputOutputMap, writerOptions)
}
private fun handleFiles(
inputOutputMap: Map,
writerOptions: Set,
): Int {
for (entry in inputOutputMap) {
val (input, output) = entry
when {
entry.isFilePair -> handleFile(input, output, writerOptions)
entry.isDirectoryPair -> handleDirectory(input, output, writerOptions)
!entry.inputExists -> {
System.err.println("${input.path} does not exist.")
return 65
}
else -> {
System.err.println(
"""
A given input and output pair (grouped positionally)
must be either files or directories.
Input is a ${if (input.isFile) "file" else "directory"}
path: ${input.absolutePath}
exists: ${input.exists()}
isWritable: ${input.canWrite()}
Output is a ${if (output.isFile) "file" else "directory"}
path: ${output.absolutePath}
exists: ${input.exists()}
isWritable: ${input.canWrite()}
Storage: ${output.usableSpace} / ${output.totalSpace} is usable.
""".trimIndent(),
)
return 65
}
}
}
return 0
}
private fun handleFile(
input: File,
output: File,
options: Set,
) {
input.inputStream().use { inputStream ->
val sizeBefore = inputStream.channel.size()
val documentBuilderFactory = DocumentBuilderFactory.newInstance()
val document = documentBuilderFactory.newDocumentBuilder().parse(input)
document.documentElement.normalize()
val rootNodes = document.childNodes.asSequence().filter { it.nodeType == Document.ELEMENT_NODE }.toList()
var graphic =
when {
rootNodes.any { it.nodeName == "svg" || input.extension == "svg" } -> {
if (outputFormat == "vd") {
ByteArrayOutputStream().use { pipeOrigin ->
val errors = Svg2Vector.parseSvgToXml(input.toPath(), pipeOrigin)
if (errors != "") {
System.err.println(
"""
Skipping ${input.path}
$errors
""".trimIndent(),
)
null
} else {
val pipeTerminal = ByteArrayInputStream(pipeOrigin.toByteArray())
val convertedDocument =
documentBuilderFactory.newDocumentBuilder().parse(pipeTerminal)
convertedDocument.documentElement.normalize()
val documentRoot =
convertedDocument.childNodes.asSequence().first {
it.nodeType == Document.ELEMENT_NODE
}
com.jzbrooks.vgo.vd.parse(documentRoot)
}
}
} else {
parse(rootNodes.first())
}
}
rootNodes.any { it.nodeName == "vector" && input.extension == "xml" } -> {
com.jzbrooks.vgo.vd.parse(rootNodes.first())
}
else -> if (input == output) return else null
}
if (graphic is VectorDrawable && outputFormat == "svg") {
graphic = graphic.toSvg()
}
val optimizationRegistry =
when (graphic) {
is VectorDrawable -> VectorDrawableOptimizationRegistry()
is ScalableVectorGraphic -> SvgOptimizationRegistry()
else -> null
}
if (graphic != null) {
optimizationRegistry?.apply(graphic)
}
if (output.parentFile?.exists() == false) output.parentFile.mkdirs()
if (!output.exists()) output.createNewFile()
output.outputStream().use { outputStream ->
if (graphic is VectorDrawable) {
val writer = VectorDrawableWriter(options)
writer.write(graphic, outputStream)
}
if (graphic is ScalableVectorGraphic) {
val writer = ScalableVectorGraphicWriter(options)
writer.write(graphic, outputStream)
}
if (graphic == null && input != output) {
inputStream.copyTo(outputStream)
}
if (printStats) {
val sizeAfter = outputStream.channel.size()
val percentSaved = ((sizeBefore - sizeAfter) / sizeBefore.toDouble()) * 100
totalBytesBefore += sizeBefore
totalBytesAfter += sizeAfter
if (percentSaved.absoluteValue > 1e-3) {
if (printFileNames) println("\n${input.path}")
println("Size before: " + formatByteDescription(sizeBefore))
println("Size after: " + formatByteDescription(sizeAfter))
println("Percent saved: $percentSaved")
}
}
}
}
}
private fun handleDirectory(
input: File,
output: File,
options: Set,
) {
assert(input.isDirectory)
assert(output.isDirectory || !output.exists())
for (file in input.walkTopDown().filter { file -> !file.isHidden && !file.isDirectory }) {
handleFile(file, File(output, file.name), options)
}
if (printStats) {
val message = "| Total bytes saved: ${(totalBytesBefore - totalBytesAfter).roundToInt()} |"
val border = "-".repeat(message.length)
println(
"""
$border
$message
$border
""".trimIndent(),
)
}
}
private fun formatByteDescription(bytes: Long): String {
return when {
bytes >= 1024 * 1024 * 1024 -> {
val gigabytes = bytes / (1024.0 * 1024.0 * 1024.0)
"%.2f GiB".format(gigabytes)
}
bytes >= 1024 * 1024 -> {
val megabytes = bytes / (1024.0 * 1024.0)
"%.2f MiB".format(megabytes)
}
bytes >= 1024 -> {
val kilobytes = bytes / 1024.0
"%.2f KiB".format(kilobytes)
}
else -> "$bytes B"
}
}
private val Map.Entry.inputExists
get() = key.exists()
private val Map.Entry.isFilePair
get() = key.isFile && (value.isFile || !value.exists())
private val Map.Entry.isDirectoryPair
get() = key.isDirectory && (value.isDirectory || !value.exists())
companion object {
private const val HELP_MESSAGE = """
> vgo [options] [file/directory]
Options:
-h --help print this message
-o --output file or directory, if not provided the input will be overwritten
-s --stats print statistics on processed files to standard out
-v --version print the version number
--indent value write files with value columns of indentation
--format value output format (svg, vd, etc)
"""
@JvmStatic
fun main(args: Array): Unit = exitProcess(Application().run(args))
}
}