===== 목적 =====
문구가 좌표상에서 어떻게 위치하는지를 파악하도록 할 것이다.
글자가 페이지상의 어느 좌표에 위치하는지를 알면, 페이지를 나누는 것이 가능해질 것이다.
===== 좌표화 하기 =====
==== 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개의 꼭지점을 만들어주면 본문의 위치가 결정되고 아래와 같이 각 구역이 나뉘어진다.
{{:android:pdfdocument:pdfpage구조.png?600|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에서 서체의 픽셀을 다루는 기준선은 아래와 같다(([[https://zzaps.tistory.com/354|궁극의 잡 블로그 참조]])).
{{:android:pdfdocument:fontmetrics.jpg?600|Font metrics}}
baseline을 기준(0)으로 위로 갈수록 음수, 아래로 갈 수록 양수라고 한다.
글자가 baseline보다 아래로 갈 수도 있음을 알 수 있다.
이에 따라서 글자나 문장의 경계선을 구할 때에도 baseline보다 아래의 위치도 고려해야 한다.
참고로, leading은 두 줄 이상일 떄 줄간격을 의미한다.
==== 2. 글자의 높이와 폭을 확인하는 함수 ====
=== 가. getTextWidths ===
각각의 글자의 폭을 개별로 계산해 주는 함수이다.
paint.getTextWidths( String text, float[] widths )(([[https://jamssoft.tistory.com/141|https://jamssoft.tistory.com/141 [What should I do?:티스토리]]]))
=== 나. mesasureText ===
float paint.measureText(String text)(([[https://jamssoft.tistory.com/141|https://jamssoft.tistory.com/141 [What should I do?:티스토리]]]))
출력하고자 하는 글자들의 전체 폭을 구하는 함수이다. 자간이 존재하므로 measureText는 getTextWidths의 전체 합보다 크다.
다음 그림은 설명의 편의를 위해 위에서 인용한 블로그에서 가져온 것이다.
{{:android:pdfdocument:measuretext.png?600|MeasureText}}
=== 다. getTextBounds ===
public void getTextBounds (String text, int start, int end, Rect bounds)
글자들의 전체 경계썬을 구해주는 함수이다. 폭의 경우 measureText와 비슷하지만 문장의 좌우양끝의 자간은 삭제하기 떄문에 measureText보다 결과값이 작다((https://stackoverflow.com/questions/7549182/android-paint-measuretext-vs-gettextbounds)).
{{:android:pdfdocument:gettextbounds.png?600|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")
아래 그림은 각각 [[http://font.junglim.com/|김정철명조체]]와 [[https://www.goryeong.go.kr/kor/contents.do?IDX=409|고령딸기체]]로 그린 글자에 대하여 경계선을 나타낸 것이다.
^ 고령딸기체 ^ 김정철명조체 ^
| {{:android:pdfdocument:딸기체.jpg?400|딸기체}} | {{:android:pdfdocument:명조체.jpg?400|명조체}} |
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로 인쇄된다.
{{:android:pdfdocument:richtext.jpg?600|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")