Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 54
ADFA-4033 | Improve OCR sanitization, value cleaners, and widget support#1333
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -27,7 +27,9 @@ interface LayoutGrammar : WidgetGrammar { | ||
| AttributeKey.GRAVITY.xmlName to CategoricalValidator(GravityValueSet.values), | ||
| AttributeKey.LAYOUT_WEIGHT.xmlName to PassThroughValidator, | ||
| AttributeKey.PADDING.xmlName to DimensionValidator, | ||
| AttributeKey.VISIBILITY.xmlName to CategoricalValidator(VisibilityValueSet.values) | ||
| AttributeKey.VISIBILITY.xmlName to CategoricalValidator(VisibilityValueSet.values), | ||
| AttributeKey.BACKGROUND.xmlName to PassThroughValidator, | ||
| AttributeKey.BACKGROUND_TINT.xmlName to PassThroughValidator | ||
| ) | ||
| } | ||
| @@ -64,10 +66,7 @@ object ImageViewGrammar : LayoutGrammar { | ||
| override val tag = "ImageView" | ||
| override val attributes = super.attributes + mapOf( | ||
| AttributeKey.SRC.xmlName to PassThroughValidator, | ||
| AttributeKey.LAYOUT_GRAVITY.xmlName to CategoricalValidator(GravityValueSet.values), | ||
| AttributeKey.BACKGROUND.xmlName to PassThroughValidator, | ||
| AttributeKey.BACKGROUND_TINT.xmlName to PassThroughValidator | ||
| AttributeKey.SRC.xmlName to PassThroughValidator | ||
| ) | ||
| } | ||
| @@ -108,3 +107,17 @@ object SliderGrammar : LayoutGrammar { | ||
| AttributeKey.STYLE.xmlName to SliderStyleValidator | ||
| ) | ||
| } | ||
| object TextViewGrammar : TextGrammar { | ||
| override val tag = "TextView" | ||
| override val attributes = super.attributes + mapOf( | ||
| AttributeKey.TEXT.xmlName to PassThroughValidator | ||
| ) | ||
| } | ||
| object ButtonGrammar : TextGrammar { | ||
| override val tag = "Button" | ||
| override val attributes = super.attributes + mapOf( | ||
| AttributeKey.TEXT.xmlName to PassThroughValidator | ||
| ) | ||
| } | ||
coderabbitai[bot] marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -25,6 +25,13 @@ object FuzzyAttributeParser { | ||
| ValueType.RAW to ValueCleaner { it } | ||
| ) | ||
| private val numericTypes = setOf( | ||
| ValueType.DIMENSION, | ||
| ValueType.SP_DIMENSION, | ||
| ValueType.INTEGER, | ||
| ValueType.FLOAT | ||
| ) | ||
| fun parse(annotation: String?, tag: String): Map<String, String> { | ||
| if (annotation.isNullOrBlank()) return emptyMap() | ||
| @@ -73,8 +80,19 @@ object FuzzyAttributeParser { | ||
| } | ||
| private fun shouldTreatTokenAsValue(token: String, currentKey: AttributeKey?): Boolean { | ||
| if (currentKey != AttributeKey.INPUT_TYPE) return false | ||
| return token.trim().lowercase() in inputTypeValues | ||
| val lowerToken = token.trim().lowercase() | ||
| return when { | ||
| currentKey == AttributeKey.INPUT_TYPE && lowerToken in inputTypeValues -> true | ||
| currentKey?.valueType == ValueType.COLOR && isColorToken(lowerToken) -> true | ||
| currentKey?.valueType == ValueType.DIMENSION && DimensionValueSet.allKeywords.any { it in lowerToken } -> true | ||
jatezzz marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| currentKey?.valueType in numericTypes -> lowerToken.any { it.isDigit() } | ||
| else -> false | ||
coderabbitai[bot] marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| } | ||
| } | ||
| private fun isColorToken(token: String): Boolean { | ||
| return token.startsWith("#") || token.startsWith("@") || token in ColorCleaner.colorMap | ||
| } | ||
| private fun flushAttribute(key: AttributeKey?, rawValue: String, tag: String, destination: MutableMap<String, String>) { | ||
| @@ -85,7 +103,9 @@ object FuzzyAttributeParser { | ||
| if (cleanedValue.isNotEmpty()) { | ||
| val (xmlAttr, finalValue) = resolveXmlAttribute(key, cleanedValue, tag) | ||
| destination[xmlAttr] = finalValue | ||
| if (!destination.containsKey(xmlAttr)) { | ||
| destination[xmlAttr] = finalValue | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -24,66 +24,49 @@ internal object TextContentCleaner : ValueCleaner { | ||
| internal object NumberCleaner : ValueCleaner { | ||
| private val ocrLetterOToZeroRegex = Regex("[oO]") | ||
| private val ocrLetterIToOneRegex = Regex("[lI]") | ||
| private val ocrLetterZToTwoRegex = Regex("[zZ]") | ||
| private val ocrLetterSToFiveRegex = Regex("[sS]") | ||
| private val ocrLetterBToSixRegex = Regex("[bB]") | ||
| private val ocrCharMap = mapOf( | ||
| 'O' to '0', 'A' to '0', '@' to '0', 'Q' to '0', | ||
| 'L' to '1', 'I' to '1', '|' to '1', '!' to '1', '/' to '1', '\\' to '1', | ||
| '(' to '1', ')' to '1', '[' to '1', ']' to '1', | ||
| 'Z' to '2', 'S' to '5', 'B' to '6' | ||
| ) | ||
| override fun clean(rawValue: String): String { | ||
| val match = Regex("-?[\\doOlIzZsSbB]+").find(rawValue) ?: return rawValue | ||
| return match.value | ||
| .replace(ocrLetterOToZeroRegex, "0") | ||
| .replace(ocrLetterIToOneRegex, "1") | ||
| .replace(ocrLetterZToTwoRegex, "2") | ||
| .replace(ocrLetterSToFiveRegex, "5") | ||
| .replace(ocrLetterBToSixRegex, "6") | ||
| val translated = rawValue.map { ocrCharMap[it.uppercaseChar()] ?: it }.joinToString("") | ||
| return Regex("-?\\d+").find(translated)?.value ?: rawValue | ||
| } | ||
| } | ||
| internal object DimensionCleaner : ValueCleaner { | ||
| private val matchKeywords = setOf("match", "parent") | ||
| private val wrapKeywords = setOf("wrap", "content", "wrapcan") | ||
| private val DIMENSION_CONSTANTS = listOf("wrap_content", "match_parent") | ||
| private val explicitDimensionRegex = Regex("^(-?\\d+)(dp|sp|px|dip)$") | ||
| private val leadingNumberRegex = Regex("^-?\\d+") | ||
| override fun clean(rawValue: String): String { | ||
| val trimmedValue = rawValue.trim() | ||
| val normalized = trimmedValue.lowercase().replace(" ", "_") | ||
| val trimmedValue = rawValue.trim().lowercase() | ||
| val normalized = trimmedValue.replace(" ", "_") | ||
| if (matchKeywords.any { it in normalized }) return "match_parent" | ||
| if (wrapKeywords.any { it in normalized }) return "wrap_content" | ||
| if (DimensionValueSet.matchKeywords.any { it in normalized }) return DimensionValueSet.MATCH_PARENT | ||
| if (DimensionValueSet.wrapKeywords.any { it in normalized }) return DimensionValueSet.WRAP_CONTENT | ||
jatezzz marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| val fuzzyResult = FuzzySearch.extractOne(normalized, DIMENSION_CONSTANTS) | ||
| val fuzzyResult = FuzzySearch.extractOne(normalized, DimensionValueSet.values) | ||
| if (fuzzyResult.score >= 60) return fuzzyResult.string | ||
| val fixedUnit = normalized.replace(Regex("0p$|op$|olp$"), "dp") | ||
| explicitDimensionRegex.matchEntire(fixedUnit)?.let { match -> | ||
| val normalizedNumber = normalizeOcrDimensionNumber(match.groupValues[1]) | ||
| return normalizedNumber + match.groupValues[2] | ||
| } | ||
| val numericPart = NumberCleaner.clean(fixedUnit.replace("_", "")) | ||
| val normalizedNumericPart = normalizeOcrDimensionNumber(numericPart) | ||
| return if (numericPart != fixedUnit) "${normalizedNumericPart}dp" else trimmedValue | ||
| } | ||
| val unitMatch = Regex("(dp|sp|px|in|mm|pt)$").find(trimmedValue) | ||
| val originalUnit = unitMatch?.value ?: "dp" | ||
| private fun normalizeOcrDimensionNumber(numericPart: String): String { | ||
| if (!numericPart.matches(Regex("-?\\d+"))) return numericPart | ||
| val firstToken = trimmedValue.substringBefore(" ") | ||
| val rawNumber = firstToken.removeSuffix(originalUnit).trim() | ||
| val numericPart = NumberCleaner.clean(rawNumber) | ||
| val isNegative = numericPart.startsWith("-") | ||
| val numericValue = numericPart.toLongOrNull() ?: return numericPart | ||
| val canonical = numericValue.toString() | ||
| val unsignedCanonical = canonical.removePrefix("-") | ||
| val numMatch = leadingNumberRegex.find(numericPart)?.value | ||
| ?: return trimmedValue | ||
| val correctedNum = removeOcrTrailingZero(numMatch) | ||
| // OCR sometimes reads the trailing "dp" as a single zero, turning 150dp into 1500. | ||
| if (unsignedCanonical.endsWith('0') && unsignedCanonical.toLong() >= 1000L) { | ||
| val normalizedValue = numericValue / 10L | ||
| return normalizedValue.toString() | ||
| } | ||
| return "$correctedNum$originalUnit" | ||
| } | ||
| return if (isNegative && numericValue == 0L) "0" else canonical | ||
| private fun removeOcrTrailingZero(num: String): String { | ||
| val isOcrArtifact = num.endsWith("0") && (num.toLongOrNull() ?: 0L) >= 1000L | ||
| return if (isOcrArtifact) num.dropLast(1) else num | ||
| } | ||
| } | ||
| @@ -96,8 +79,10 @@ internal object SpDimensionCleaner : ValueCleaner { | ||
| } | ||
| internal object ColorCleaner : ValueCleaner { | ||
| private val colorMap = mapOf( | ||
| "red" to "#FF0000", "rel" to "#FF0000", "green" to "#00FF00", "blue" to "#0000FF", | ||
| val colorMap = mapOf( | ||
| "red" to "#FF0000", "rel" to "#FF0000", "rad" to "#FF0000", "reo" to "#FF0000", | ||
| "green" to "#00FF00", | ||
| "blue" to "#0000FF", "ine" to "#0000FF", "hne" to "#0000FF", "hlue" to "#0000FF", "ane" to "#0000FF", "lne" to "#0000FF", | ||
| "black" to "#000000", "white" to "#FFFFFF", "gray" to "#808080", | ||
| "grey" to "#808080", "dark_gray" to "#A9A9A9", "yellow" to "#FFFF00", | ||
| "cyan" to "#00FFFF", "magenta" to "#FF00FF", "purple" to "#800080", | ||
| @@ -109,13 +94,14 @@ internal object ColorCleaner : ValueCleaner { | ||
| override fun clean(rawValue: String): String { | ||
| if (rawValue.startsWith("#") || rawValue.startsWith("@")) return rawValue | ||
| val normalizedValue = rawValue.lowercase().replace(" ", "_") | ||
| val normalizedValue = rawValue.lowercase().replace(Regex("[^a-z_]"), "").replace(" ", "_") | ||
| val exactColor = colorMap[normalizedValue] | ||
| if (exactColor != null) return exactColor | ||
| val result = FuzzySearch.extractOne(normalizedValue, colorMap.keys.toList()) | ||
| return if (result.score >= 75) colorMap[result.string] ?: rawValue else rawValue | ||
| return if (result.score >= 70) colorMap[result.string] ?: rawValue else rawValue | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -3,8 +3,9 @@ package org.appdevforall.codeonthego.computervision.domain.parser.sanitizer | ||
| class ColorSanitizer : DictionaryRegexSanitizer() { | ||
| override val rawRules = mapOf( | ||
| "backgroundired" to "background red", | ||
| "backgroundred" to "background red" | ||
| "backgroundired" to "background: red", | ||
| "backgroundred" to "background: red", | ||
| "\\bback[a-z]*[-_.]?\\s*[:;]\\s*" to "background: " | ||
| ) | ||
| } | ||
| @@ -16,8 +17,8 @@ class TextAttributeSanitizer : DictionaryRegexSanitizer() { | ||
| class DimensionSanitizer : DictionaryRegexSanitizer() { | ||
| override val rawRules = mapOf( | ||
| "[il]ayout\\.?\\s*w[io]l?[td]h\\.?" to "layout_width:", | ||
| "layout\\s*hei[sck]+t\\.?" to "layout_height:", | ||
| "[il]ay[a-z]*[-_.\\s]*w[a-z0-9]*\\.?\\s*[:;]\\s*" to "layout_width:", | ||
| "[il]ay[a-z]*[-_.\\s]*hei[a-z0-9]*\\.?\\s*[:;]\\s*" to "layout_height:", | ||
jatezzz marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| "m?w?at[ce]h[-_\\s]?p[ar]+ent" to "match_parent" | ||
| ) | ||
| } | ||
| @@ -31,6 +32,8 @@ class MarginPaddingSanitizer : DictionaryRegexSanitizer() { | ||
| class StructureSanitizer : DictionaryRegexSanitizer() { | ||
| override val rawRules = mapOf( | ||
| "horizontal\\s+gravity\\s*:\\s*center\\s+layout" to "layout_gravity: center_horizontal" | ||
| "horizontal\\s+gravity\\s*:\\s*center\\s+layout" to "layout_gravity: center_horizontal", | ||
| "\\b[ilL][dl]\\b\\s*[:;]?" to "id: ", | ||
| "\\bS[ec][rt]\\b\\s*[:;]?" to "src: " | ||
| ) | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.