Skip to content
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

PAINTROID-684 Apply outline to text #1330

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ import android.graphics.Paint
import android.graphics.PointF
import android.graphics.Typeface
import android.widget.EditText
import android.widget.RelativeLayout
import androidx.core.content.res.ResourcesCompat
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.Espresso
import androidx.test.espresso.Espresso.onView
Expand Down Expand Up @@ -61,6 +63,7 @@ import org.catrobat.paintroid.tools.FontType
import org.catrobat.paintroid.tools.ToolReference
import org.catrobat.paintroid.tools.ToolType
import org.catrobat.paintroid.tools.implementation.BOX_OFFSET
import org.catrobat.paintroid.tools.implementation.DEFAULT_TEXT_OUTLINE_WIDTH
import org.catrobat.paintroid.tools.implementation.MARGIN_TOP
import org.catrobat.paintroid.tools.implementation.TEXT_SIZE_MAGNIFICATION_FACTOR
import org.catrobat.paintroid.tools.implementation.TextTool
Expand Down Expand Up @@ -90,6 +93,8 @@ class TextToolIntegrationTest {
private var underlinedToggleButton: MaterialButton? = null
private var italicToggleButton: MaterialButton? = null
private var boldToggleButton: MaterialButton? = null
private var outlineToggleButton: MaterialButton? = null
private var outlineWidthLayout: RelativeLayout? = null
private var textSize: EditText? = null
private var layerModel: LayerContracts.Model? = null
private lateinit var activity: MainActivity
Expand All @@ -116,6 +121,8 @@ class TextToolIntegrationTest {
activity.findViewById(R.id.pocketpaint_text_tool_dialog_toggle_underlined)
italicToggleButton = activity.findViewById(R.id.pocketpaint_text_tool_dialog_toggle_italic)
boldToggleButton = activity.findViewById(R.id.pocketpaint_text_tool_dialog_toggle_bold)
outlineToggleButton = activity.findViewById(R.id.pocketpaint_text_tool_dialog_toggle_outline)
outlineWidthLayout = activity.findViewById(R.id.pocketpaint_outline_width_layout)
textSize = activity.findViewById(R.id.pocketpaint_font_size_text)
textTool?.resetBoxPosition()
}
Expand All @@ -125,6 +132,7 @@ class TextToolIntegrationTest {
selectFormatting(FormattingOptions.ITALIC)
selectFormatting(FormattingOptions.BOLD)
selectFormatting(FormattingOptions.UNDERLINE)
selectFormatting(FormattingOptions.OUTLINE)
enterTestText()
onView(withId(R.id.pocketpaint_text_tool_dialog_input_text)).perform(click())
onView(withId(R.id.pocketpaint_text_tool_dialog_input_text)).perform(
Expand All @@ -135,12 +143,14 @@ class TextToolIntegrationTest {
italicToggleButton?.let { Assert.assertTrue(it.isChecked) }
boldToggleButton?.let { Assert.assertTrue(it.isChecked) }
underlinedToggleButton?.let { Assert.assertTrue(it.isChecked) }
outlineToggleButton?.let { Assert.assertTrue(it.isChecked) }
Assert.assertEquals(TEST_TEXT_ADVANCED, textEditText?.text?.toString())
onView(withId(R.id.pocketpaint_text_tool_dialog_input_text)).check(matches(isDisplayed()))
onView(withId(R.id.pocketpaint_text_tool_dialog_list_font)).check(matches(isDisplayed()))
onView(withId(R.id.pocketpaint_text_tool_dialog_toggle_underlined)).check(matches(isDisplayed()))
onView(withId(R.id.pocketpaint_text_tool_dialog_toggle_italic)).check(matches(isDisplayed()))
onView(withId(R.id.pocketpaint_text_tool_dialog_toggle_bold)).check(matches(isDisplayed()))
onView(withId(R.id.pocketpaint_text_tool_dialog_toggle_outline)).check(matches(isDisplayed()))
onView(withId(R.id.pocketpaint_font_size_text)).check(matches(isDisplayed()))
}

Expand All @@ -149,13 +159,15 @@ class TextToolIntegrationTest {
selectFormatting(FormattingOptions.ITALIC)
selectFormatting(FormattingOptions.BOLD)
selectFormatting(FormattingOptions.UNDERLINE)
selectFormatting(FormattingOptions.OUTLINE)
enterTestText()
onDrawingSurfaceView()
.perform(UiInteractions.touchAt(DrawingSurfaceLocationProvider.MIDDLE))

italicToggleButton?.let { Assert.assertTrue(it.isChecked) }
boldToggleButton?.let { Assert.assertTrue(it.isChecked) }
underlinedToggleButton?.let { Assert.assertTrue(it.isChecked) }
outlineToggleButton?.let { Assert.assertTrue(it.isChecked) }
Assert.assertEquals(TEST_TEXT, textEditText?.text?.toString())
onView(withId(R.id.pocketpaint_text_tool_dialog_input_text))
.check(matches(not(isDisplayed())))
Expand All @@ -167,6 +179,8 @@ class TextToolIntegrationTest {
.check(matches(not(isDisplayed())))
onView(withId(R.id.pocketpaint_text_tool_dialog_toggle_bold))
.check(matches(not(isDisplayed())))
onView(withId(R.id.pocketpaint_text_tool_dialog_toggle_outline))
.check(matches(not(isDisplayed())))
onView(withId(R.id.pocketpaint_font_size_text))
.check(matches(not(isDisplayed())))
}
Expand Down Expand Up @@ -247,6 +261,8 @@ class TextToolIntegrationTest {
textTool?.let { Assert.assertFalse(it.underlined) }
textTool?.let { Assert.assertFalse(it.italic) }
textTool?.let { Assert.assertFalse(it.bold) }
textTool?.let { Assert.assertFalse(it.outlined) }
outlineWidthLayout?.let { Assert.assertFalse(it.isVisible) }
}

@Test
Expand Down Expand Up @@ -292,6 +308,20 @@ class TextToolIntegrationTest {
Assert.assertFalse(toolMemberBold)
boldToggleButton?.let { Assert.assertFalse(it.isChecked) }
Assert.assertEquals(getFormattingOptionAsString(FormattingOptions.BOLD), boldToggleButton?.text.toString())
selectFormatting(FormattingOptions.OUTLINE)
textTool?.let { Assert.assertTrue(it.outlined) }
outlineToggleButton?.let { Assert.assertTrue(it.isChecked) }
Assert.assertEquals(
getFormattingOptionAsString(FormattingOptions.OUTLINE),
outlineToggleButton?.text.toString()
)
selectFormatting(FormattingOptions.OUTLINE)
textTool?.let { Assert.assertFalse(it.outlined) }
outlineToggleButton?.let { Assert.assertFalse(it.isChecked) }
Assert.assertEquals(
getFormattingOptionAsString(FormattingOptions.OUTLINE),
outlineToggleButton?.text.toString()
)
}

@Test
Expand All @@ -301,6 +331,7 @@ class TextToolIntegrationTest {
selectFormatting(FormattingOptions.UNDERLINE)
selectFormatting(FormattingOptions.ITALIC)
selectFormatting(FormattingOptions.BOLD)
selectFormatting(FormattingOptions.OUTLINE)
onToolBarView().performCloseToolOptionsView()

val oldBoxWidth = toolMemberBoxWidth
Expand All @@ -318,6 +349,7 @@ class TextToolIntegrationTest {
underlinedToggleButton?.let { Assert.assertTrue(it.isChecked) }
italicToggleButton?.let { Assert.assertTrue(it.isChecked) }
boldToggleButton?.let { Assert.assertTrue(it.isChecked) }
outlineToggleButton?.let { Assert.assertTrue(it.isChecked) }
Assert.assertTrue(oldBoxWidth == toolMemberBoxWidth && oldBoxHeight == toolMemberBoxHeight)
}

Expand All @@ -328,6 +360,7 @@ class TextToolIntegrationTest {
selectFormatting(FormattingOptions.UNDERLINE)
selectFormatting(FormattingOptions.ITALIC)
selectFormatting(FormattingOptions.BOLD)
selectFormatting(FormattingOptions.OUTLINE)

val toolMemberBoxPosition = toolMemberBoxPosition
val expectedPosition = toolMemberBoxPosition?.y?.let { PointF(toolMemberBoxPosition.x, it) }
Expand All @@ -344,6 +377,7 @@ class TextToolIntegrationTest {
underlinedToggleButton?.let { Assert.assertTrue(it.isChecked) }
italicToggleButton?.let { Assert.assertTrue(it.isChecked) }
boldToggleButton?.let { Assert.assertTrue(it.isChecked) }
outlineToggleButton?.let { Assert.assertTrue(it.isChecked) }
Assert.assertEquals(expectedPosition, toolMemberBoxPosition)
Assert.assertEquals(oldBoxWidth.toDouble(), toolMemberBoxWidth.toDouble(), EQUALS_DELTA)
Assert.assertEquals(oldBoxHeight.toDouble(), toolMemberBoxHeight.toDouble(), EQUALS_DELTA)
Expand Down Expand Up @@ -750,6 +784,7 @@ class TextToolIntegrationTest {
selectFormatting(FormattingOptions.ITALIC)
selectFormatting(FormattingOptions.BOLD)
selectFormatting(FormattingOptions.UNDERLINE)
selectFormatting(FormattingOptions.OUTLINE)
}
val boxWidth = toolMemberBoxWidth
val boxHeight = toolMemberBoxHeight
Expand All @@ -759,10 +794,120 @@ class TextToolIntegrationTest {
selectFormatting(FormattingOptions.ITALIC)
selectFormatting(FormattingOptions.BOLD)
selectFormatting(FormattingOptions.UNDERLINE)
selectFormatting(FormattingOptions.OUTLINE)
Assert.assertTrue(boxWidth < toolMemberBoxWidth && boxHeight < toolMemberBoxHeight)
}
}

@Test
fun testTextOutlineMode() {
enterTestText()
val canvasPoint = centerBox()
onTopBarView().performClickCheckmark()
val surfaceBitmapWidth = layerModel?.width
val pixelsDrawingSurface = surfaceBitmapWidth?.let { IntArray(it) }
if (surfaceBitmapWidth != null && canvasPoint != null) {
layerModel?.currentLayer?.bitmap?.getPixels(
pixelsDrawingSurface, 0, surfaceBitmapWidth, 0,
canvasPoint.y.toInt(), surfaceBitmapWidth, 1
)
}
val blackPixelAmountNoOutline = pixelsDrawingSurface?.let { countPixelsWithColor(it, Color.BLACK) }
val whitePixelAmountNoOutline = pixelsDrawingSurface?.let { countPixelsWithColor(it, Color.WHITE) }
onTopBarView().performUndo()

selectFormatting(FormattingOptions.OUTLINE)
textTool?.let { Assert.assertTrue(it.outlined) }

onTopBarView().performClickCheckmark()

if (surfaceBitmapWidth != null && canvasPoint != null) {
layerModel?.currentLayer?.bitmap?.getPixels(
pixelsDrawingSurface, 0, surfaceBitmapWidth, 0,
canvasPoint.y.toInt(), surfaceBitmapWidth, 1
)
}

val blackPixelAmountWithOutline = pixelsDrawingSurface?.let { countPixelsWithColor(it, Color.BLACK) }
if (blackPixelAmountNoOutline != null && blackPixelAmountWithOutline != null) {
assert(blackPixelAmountNoOutline > blackPixelAmountWithOutline)
assert(blackPixelAmountWithOutline > 0)
}

val whitePixelAmountWithOutline = pixelsDrawingSurface?.let { countPixelsWithColor(it, Color.WHITE) }
if (whitePixelAmountNoOutline != null && whitePixelAmountWithOutline != null) {
assert(whitePixelAmountNoOutline < whitePixelAmountWithOutline)
}
}

@Test
fun testTextOutlineWidth() {
val canvasPoint = centerBox()
selectFormatting(FormattingOptions.OUTLINE)
textTool?.let { Assert.assertTrue(it.outlined) }
outlineWidthLayout?.let { Assert.assertTrue(it.isVisible) }
val outlineWidthInput = onView(withId(R.id.pocketpaint_outline_width_text))
val outlineWidthSeekbar = onView(withId(R.id.pocketpaint_outline_width_seek_bar))
outlineWidthInput.check(matches(ViewMatchers.withText(DEFAULT_TEXT_OUTLINE_WIDTH.toString())))
outlineWidthSeekbar.check(matches(UiMatcher.withProgress(DEFAULT_TEXT_OUTLINE_WIDTH)))

enterTestText()

var testOutlineWidthText = "1"

outlineWidthInput.perform(
ViewActions.replaceText(testOutlineWidthText),
ViewActions.closeSoftKeyboard()
)
outlineWidthInput.check(matches(ViewMatchers.withText(testOutlineWidthText)))
outlineWidthSeekbar.check(matches(UiMatcher.withProgress(testOutlineWidthText.toInt())))

onTopBarView().performClickCheckmark()
val surfaceBitmapWidth = layerModel?.width
val pixelsDrawingSurface = surfaceBitmapWidth?.let { IntArray(it) }
if (surfaceBitmapWidth != null && canvasPoint != null) {
layerModel?.currentLayer?.bitmap?.getPixels(
pixelsDrawingSurface, 0, surfaceBitmapWidth, 0,
canvasPoint.y.toInt(), surfaceBitmapWidth, 1
)
}
val blackPixelAmountThinOutline = pixelsDrawingSurface?.let { countPixelsWithColor(it, Color.BLACK) }
val whitePixelAmountThinOutline = pixelsDrawingSurface?.let { countPixelsWithColor(it, Color.WHITE) }
onTopBarView().performUndo()

testOutlineWidthText = "60"

outlineWidthInput.perform(
ViewActions.replaceText(testOutlineWidthText),
ViewActions.closeSoftKeyboard()
)
outlineWidthInput.check(matches(ViewMatchers.withText(testOutlineWidthText)))
outlineWidthSeekbar.check(matches(UiMatcher.withProgress(testOutlineWidthText.toInt())))

onTopBarView().performClickCheckmark()

if (surfaceBitmapWidth != null && canvasPoint != null) {
layerModel?.currentLayer?.bitmap?.getPixels(
pixelsDrawingSurface, 0, surfaceBitmapWidth, 0,
canvasPoint.y.toInt(), surfaceBitmapWidth, 1
)
}

val blackPixelAmountThickOutline = pixelsDrawingSurface?.let { countPixelsWithColor(it, Color.BLACK) }
if (blackPixelAmountThinOutline != null && blackPixelAmountThickOutline != null) {
assert(blackPixelAmountThinOutline > 0)
assert(blackPixelAmountThickOutline > 0)
assert(blackPixelAmountThickOutline > blackPixelAmountThinOutline)
}

val whitePixelAmountThickOutline = pixelsDrawingSurface?.let { countPixelsWithColor(it, Color.WHITE) }
if (whitePixelAmountThinOutline != null && whitePixelAmountThickOutline != null) {
assert(whitePixelAmountThinOutline > 0)
assert(whitePixelAmountThickOutline > 0)
assert(whitePixelAmountThickOutline < whitePixelAmountThinOutline)
}
}

private fun centerBox(): PointF? {
val screenPoint =
activityHelper?.displayWidth?.div(2.0f)
Expand Down Expand Up @@ -892,6 +1037,7 @@ class TextToolIntegrationTest {
FormattingOptions.UNDERLINE -> activity.getString(R.string.text_tool_dialog_underline_shortcut)
FormattingOptions.ITALIC -> activity.getString(R.string.text_tool_dialog_italic_shortcut)
FormattingOptions.BOLD -> activity.getString(R.string.text_tool_dialog_bold_shortcut)
FormattingOptions.OUTLINE -> activity.getString(R.string.text_tool_dialog_outline_shortcut)
}
}

Expand Down Expand Up @@ -921,7 +1067,7 @@ class TextToolIntegrationTest {
private val toolMemberMultilineText: Array<String>
get() = textTool!!.multilineText

private enum class FormattingOptions { UNDERLINE, ITALIC, BOLD }
private enum class FormattingOptions { UNDERLINE, ITALIC, BOLD, OUTLINE }

companion object {
private const val TEST_TEXT = "123 www 123"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,9 @@ class CommandSerializationTest {
underline = false,
italic = true,
textSize = 25f,
textSkewX = -0.25f
textSkewX = -0.25f,
outlined = false,
outlineWidth = 25
)

expectedModel.commands.add(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,16 @@
package org.catrobat.paintroid.command.implementation

import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.PointF
import org.catrobat.paintroid.command.Command
import org.catrobat.paintroid.command.serialization.SerializableTypeface
import org.catrobat.paintroid.common.ITALIC_FONT_BOX_ADJUSTMENT
import org.catrobat.paintroid.contract.LayerContracts
import org.catrobat.paintroid.tools.implementation.OUTLINED_FONT_WIDTH_ADJUSTMENT

const val TEXT_SIZE_MAGNIFICATION_FACTOR = 3f

class TextToolCommand(
multilineText: Array<String>,
Expand Down Expand Up @@ -74,14 +78,30 @@ class TextToolCommand(
val scaledBoxWidth = boxWidth / widthScaling
val scaledBoxHeight = boxHeight / heightScaling

val fillPaint = Paint(textPaint)
if (typeFaceInfo.outlined) fillPaint.color = Color.WHITE
multilineText.forEachIndexed { index, textLine ->
canvas.drawText(
textLine,
scaledWidthOffset - scaledBoxWidth / 2 / if (typeFaceInfo.italic) ITALIC_FONT_BOX_ADJUSTMENT else 1f,
-(scaledBoxHeight / 2) + scaledHeightOffset - textAscent + lineHeight * index,
textPaint
fillPaint
)
}
if (typeFaceInfo.outlined) {
val outlinePaint = Paint(textPaint)
val adjustedStrokeWidth = if (typeFaceInfo.outlineWidth == 0) 0f else java.lang.Float.max(textPaint.textSize / TEXT_SIZE_MAGNIFICATION_FACTOR * (typeFaceInfo.outlineWidth / OUTLINED_FONT_WIDTH_ADJUSTMENT), 1f)
outlinePaint.style = Paint.Style.STROKE
outlinePaint.strokeWidth = adjustedStrokeWidth
multilineText.forEachIndexed { index, textLine ->
canvas.drawText(
textLine,
scaledWidthOffset - scaledBoxWidth / 2 / if (typeFaceInfo.italic) ITALIC_FONT_BOX_ADJUSTMENT else 1f,
-(scaledBoxHeight / 2) + scaledHeightOffset - textAscent + lineHeight * index,
outlinePaint
)
}
}
restore()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import com.esotericsoftware.kryo.io.Input
import com.esotericsoftware.kryo.io.Output
import org.catrobat.paintroid.tools.FontType

data class SerializableTypeface(val font: FontType, val bold: Boolean, val underline: Boolean, val italic: Boolean, val textSize: Float, val textSkewX: Float) {
data class SerializableTypeface(val font: FontType, val bold: Boolean, val underline: Boolean, val italic: Boolean, val textSize: Float, val textSkewX: Float, val outlined: Boolean, val outlineWidth: Int) {

class TypefaceSerializer(version: Int) : VersionSerializer<SerializableTypeface>(version) {
override fun write(kryo: Kryo, output: Output, typeface: SerializableTypeface) {
Expand All @@ -34,6 +34,8 @@ data class SerializableTypeface(val font: FontType, val bold: Boolean, val under
writeBoolean(typeface.italic)
writeFloat(typeface.textSize)
writeFloat(typeface.textSkewX)
writeBoolean(typeface.outlined)
writeInt(typeface.outlineWidth)
}
}

Expand All @@ -42,7 +44,7 @@ data class SerializableTypeface(val font: FontType, val bold: Boolean, val under

override fun readCurrentVersion(kryo: Kryo, input: Input, type: Class<out SerializableTypeface>): SerializableTypeface {
return with(input) {
SerializableTypeface(FontType.valueOf(readString()), readBoolean(), readBoolean(), readBoolean(), readFloat(), readFloat())
SerializableTypeface(FontType.valueOf(readString()), readBoolean(), readBoolean(), readBoolean(), readFloat(), readFloat(), readBoolean(), readInt())
}
}
}
Expand Down
Loading