===== 목적 ===== 문구가 좌표상에서 어떻게 위치하는지를 파악하도록 할 것이다. 글자가 페이지상의 어느 좌표에 위치하는지를 알면, 페이지를 나누는 것이 가능해질 것이다. ===== 좌표화 하기 ===== ==== 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")