목차

목적

문구가 좌표상에서 어떻게 위치하는지를 파악하도록 할 것이다.

글자가 페이지상의 어느 좌표에 위치하는지를 알면, 페이지를 나누는 것이 가능해질 것이다.

좌표화 하기

1. 2차원 좌표계 클래스 만들기

다음과 같이 2차원 좌표계 클래스를 만들었다.

// 2차원 위치 POS
data class POS(private val x : Int = 0, private val y : Int = 0) {
    var X: Int = x
    var Y: Int = y
 
    operator fun plus(increment : POS) : POS {
        return POS(X + increment.Y, Y + increment.Y)
    }
 
    operator fun minus(decrease : POS) : POS {
        return POS(X - decrease.Y, Y - decrease.Y)
    }
}

연산자에 대한 오버로딩은 앞에 operator를 붙이면 된다. 연사자 오버로딩을 통하여 2차원 좌표계의 덧셈을 편하게 만들었다. 위의 코드를 참조하라.

2. 좌표 정의

이전 포스팅에서는 A4 용지의 크기를 포인트 단위로 정했었다. 이러한 A4의 크기 내에서 머리말, 꼬리말, 좌우여백을 다음과 같이 만들었다. 그리고 글을 쓸 시작 시점은 당연히 본문 부분의 좌측 최상단일 것이다. 본문부분의 좌측 최상단은 머리말과 좌측 여백만큼 오프셋을 계산하면 될 것이다.

    // 여백
    val marginTop = 42
    val marginBottom = 50
    val marginLeft = 47
    val marginRight = 48
 
    // 좌표 위치
    private var totalPOS = POS(marginLeft, marginTop)
    private var currentPOS = POS(marginLeft, marginTop)

totalPOS은 페이지 내의 상대위치가 아닌 절대 위치를 말한다. 그리고 currentPOS은 페이지 내에서의 상대위치를 가리키는 좌표이다.

3. 페이지 내 구역을 살펴보기

위와 같이 4개의 꼭지점을 만들어주면 본문의 위치가 결정되고 아래와 같이 각 구역이 나뉘어진다.

PDF Page구조

위치 이동 함수 만들기

아래의 두개의 함수로 글자의 위치를 조정할 것이다.

1. 줄 바뀜 함수 만들기

워드프로세서에서 엔터를 치면 줄을 한칸 아래로 내리는 함수를 대충 만들어 보았다.

    // Line Break : Veritcal Movement
    fun linefeed() {
        totalPOS += POS(0, 50)
        currentPOS += POS(0, 50)
    }

2. 스페이스 함수 만들기

한칸 오른쪽으로 옮기는 함수를 만들었다.

    // Space  : Horizontal Movement
    fun space() {
        totalPOS += POS(20, 0)
        currentPOS += POS(20, 0)
    }

글자의 경계선 확인하기

1. Font metrics 이해

Android graphics API에서 서체의 픽셀을 다루는 기준선은 아래와 같다1).

Font metrics

baseline을 기준(0)으로 위로 갈수록 음수, 아래로 갈 수록 양수라고 한다.

글자가 baseline보다 아래로 갈 수도 있음을 알 수 있다.

이에 따라서 글자나 문장의 경계선을 구할 때에도 baseline보다 아래의 위치도 고려해야 한다.

참고로, leading은 두 줄 이상일 떄 줄간격을 의미한다.

2. 글자의 높이와 폭을 확인하는 함수

가. getTextWidths

각각의 글자의 폭을 개별로 계산해 주는 함수이다.

paint.getTextWidths( String text, float[] widths )2)

나. mesasureText

float paint.measureText(String text)3)

출력하고자 하는 글자들의 전체 폭을 구하는 함수이다. 자간이 존재하므로 measureText는 getTextWidths의 전체 합보다 크다.

다음 그림은 설명의 편의를 위해 위에서 인용한 블로그에서 가져온 것이다.

MeasureText

다. getTextBounds

public void getTextBounds (String text, int start, int end, Rect bounds)

글자들의 전체 경계썬을 구해주는 함수이다. 폭의 경우 measureText와 비슷하지만 문장의 좌우양끝의 자간은 삭제하기 떄문에 measureText보다 결과값이 작다4).

getTextbounds

그런데 경계선을 구해주는 이 함수는 정확히 끝과 끝을 나타내주는 것은 아니다. 아마도 fontmetrics에서 baseline이라는 개념떄문에 그런것 아닌가 싶다.

그렇기 떄문에 아래에서는 정확히 어떤 부분을 나타내는 지를 확인해보고자 한다.

3. 글자의 경계확인하는 함수

위의 getTextBounds를 통해 다음과 같이 상자와 선을 그려 보았다.

가. 상자와 선을 그리는 함수

다음과 같이 상자와 선을 그리는 함수를 만들었다.

    // Text with underline, rectangle, outline
    fun richText(text : String, canvas: Canvas, switch : String) {
        title.setTypeface(fontStrawberry)
        title.color = ContextCompat.getColor(context, R.color.orange_80)
        title.textSize = 16f
        title.textAlign = Paint.Align.LEFT
 
        // Draw Text
        canvas.drawText(text, currentPOS.X.toFloat(), currentPOS.Y.toFloat(), title)
 
        // Font Effect
        when (switch) {
            "Rect" -> {
                val bounds = Rect()
                title.getTextBounds(text, 0, text.length, bounds)
                val rc = Rect(currentPOS.X, currentPOS.Y - bounds.height(), currentPOS.X + bounds.right, currentPOS.Y - bounds.height())
 
                canvas.drawRect(rc, linePaint)
            }
            "LineBottom" -> {
                val bounds = Rect()
                title.getTextBounds(text, 0, text.length, bounds)
                canvas.drawLine(currentPOS.X.toFloat(), currentPOS.Y.toFloat() + bounds.bottom.toFloat(), currentPOS.X + bounds.right.toFloat(), currentPOS.Y + bounds.bottom.toFloat(), linePaint)
 
                canvas.drawText("Bottom : " + bounds.bottom, currentPOS.X + bounds.right.toFloat() + 50, currentPOS.Y.toFloat(), textPaint)
            }
            "LineTop" -> {
                val bounds = Rect()
                title.getTextBounds(text, 0, text.length, bounds)
                canvas.drawLine(currentPOS.X.toFloat(), currentPOS.Y.toFloat() + bounds.top.toFloat(), currentPOS.X + bounds.right.toFloat(), currentPOS.Y + bounds.top.toFloat(), linePaint)
 
                canvas.drawText("Top : " + bounds.top, currentPOS.X + bounds.right.toFloat() + 50, currentPOS.Y.toFloat(), textPaint)
            }
            "LineHeight" ->  {
                val bounds = Rect()
                title.getTextBounds(text, 0, text.length, bounds)
                canvas.drawLine(currentPOS.X.toFloat(), currentPOS.Y.toFloat() - bounds.height().toFloat(), currentPOS.X + bounds.right.toFloat(), currentPOS.Y - bounds.height().toFloat(), linePaint)
 
                canvas.drawText("Height(minus) : " + -bounds.height(), currentPOS.X + bounds.right.toFloat() + 50, currentPOS.Y.toFloat(), textPaint)
            }
 
        }
 
    }

getTextBounds에서 top과 bottom, 그리고 줄의 높이인 height()가 어떻 결과값을 나타내는지 확인하게 만들었다.

나. 호출하기

다음과 같이 호출하였다.

    // 상자만들기
    pdfUtil.linefeed()
    pdfUtil.titleText("글자의 크기를 재봅니다",  canvas, true)
 
    pdfUtil.space()
    // 선을 그리기
    pdfUtil.linefeed()
    pdfUtil.richText("Text Bounds : Bottom", canvas, "LineBottom")
 
    pdfUtil.linefeed()
    pdfUtil.richText("Text Bounds : Top", canvas, "LineTop")
 
    pdfUtil.linefeed()
    pdfUtil.richText("Text Bounds : Height", canvas, "LineHeight")
 
    pdfUtil.linefeed()
    pdfUtil.richText("텍스트 경계선 : 바닥", canvas, "LineBottom")
 
    pdfUtil.linefeed()
    pdfUtil.richText("텍스트 경계선 : 천장", canvas, "LineTop")
 
    pdfUtil.linefeed()
    pdfUtil.richText("텍스트 경계썬 : 줄의 높이", canvas, "LineHeight")

아래 그림은 각각 김정철명조체고령딸기체로 그린 글자에 대하여 경계선을 나타낸 것이다.

고령딸기체 김정철명조체
딸기체 명조체

LineHeight()로 줄의 높이를 정하는게 가장 시인성 있는 것을 알 수 있다. 따라서 앞으로는 이를 기준으로 글자의 높이를 정하겠다.

결론

1. Rich Text

다음과 같이 Rich Text를 구현할 수 있다.

    // Text with underline, rectangle, outline, shadow
    fun richText(text : String, canvas: Canvas, switch : String) {
        title.setTypeface(fontKJCMyungjo)
        title.color = ContextCompat.getColor(context, R.color.orange_80)
        title.textSize = 16f
        title.textAlign = Paint.Align.LEFT
 
        // Font Effect
        when (switch) {
            "Rect" -> {
                // Draw Text
                canvas.drawText(text, currentPOS.X.toFloat(), currentPOS.Y.toFloat(), title)
 
                val bounds = Rect()
                title.getTextBounds(text, 0, text.length, bounds)
                val offset = 2
                val rc = Rect(currentPOS.X - offset, currentPOS.Y - bounds.height() + offset, currentPOS.X + bounds.right + offset, currentPOS.Y + bounds.bottom + offset)
 
                canvas.drawRect(rc, linePaint)
            }
            "LineBottom" ->  {
                // Draw Text
                canvas.drawText(text, currentPOS.X.toFloat(), currentPOS.Y.toFloat(), title)
                val bounds = Rect()
                title.getTextBounds(text, 0, text.length, bounds)
                val offset = 2f
                canvas.drawLine(currentPOS.X.toFloat() - offset, currentPOS.Y.toFloat() + bounds.bottom + offset, currentPOS.X + bounds.right.toFloat() + offset, currentPOS.Y + bounds.bottom + offset, linePaint)
 
                canvas.drawText("Height(minus) : " + -bounds.height(), currentPOS.X + bounds.right.toFloat() + 50, currentPOS.Y.toFloat(), textPaint)
            }
            "LineCenter" ->  {
                // Draw Text
                canvas.drawText(text, currentPOS.X.toFloat(), currentPOS.Y.toFloat(), title)
 
                val bounds = Rect()
                title.getTextBounds(text, 0, text.length, bounds)
                val offset = 2f
                canvas.drawLine(currentPOS.X.toFloat() - offset, currentPOS.Y.toFloat() - (bounds.height() / 2).toFloat() + bounds.bottom, currentPOS.X + bounds.right.toFloat() + offset, currentPOS.Y - (bounds.height()  / 2).toFloat() + bounds.bottom, linePaint)
 
                canvas.drawText("Center : " + -bounds.height(), currentPOS.X + bounds.right.toFloat() + 50, currentPOS.Y.toFloat(), textPaint)
            }
            "OutlineFill" -> {
                // draw outline of text
                title.style = Paint.Style.FILL_AND_STROKE
                title.strokeWidth = 1f
                title.color = ContextCompat.getColor(context, R.color.teal_700)
 
                canvas.drawText( text, currentPOS.X.toFloat(), currentPOS.Y.toFloat(), title )
            }
            "Outline" -> {
                // draw outline of text
                title.style = Paint.Style.STROKE
                title.strokeWidth = 0.5f
                title.color = ContextCompat.getColor(context, R.color.teal_700)
 
                canvas.drawText( text, currentPOS.X.toFloat(), currentPOS.Y.toFloat(), title )
            }
            "Shadow" -> {
                title.style = Paint.Style.FILL
                title.strokeWidth = 1f
                title.setShadowLayer(1f, 1f, 1f, ContextCompat.getColor(context, R.color.black))
                canvas.drawText( text, currentPOS.X.toFloat(), currentPOS.Y.toFloat(), title )
            }
 
        }
 
    }

2. 예시

위의 글자꾸미기는 다음과 같이 PDF로 인쇄된다.

Rich Text

    pdfUtil.space()
    // 선을 그리기
    pdfUtil.linefeed()
    pdfUtil.richText("상자를 그립니다 : Rect", canvas, "Rect")
 
    pdfUtil.linefeed()
    pdfUtil.richText("밑줄을 그립니다 : LineBottom", canvas, "LineBottom")
 
    pdfUtil.linefeed()
    pdfUtil.richText("취소선을 그립니다 : LineCenter", canvas, "LineCenter")
 
    pdfUtil.linefeed()
    pdfUtil.richText("외곽선을 그리고 채웁니다. : OutlineFill", canvas, "OutlineFill")
 
    pdfUtil.linefeed()
    pdfUtil.richText("외곽선만 그립니다. : OutlineFill", canvas, "Outline")
 
    pdfUtil.linefeed()
    pdfUtil.richText("그림자를 그립니다. : OutlineFill", canvas, "Shadow")