===== 목적 ===== 다음의 그림을 다시 보자 {{:android:pdfdocument:pdfpage구조.png?400|pdf page구조}} 통상의 문서를 머리말과 꼬리말, 본문, 그리고 좌우여백으로 이루어진다. 지금까지는 위의 구역을 나누지 않고 전체 페이지를 기준으로 좌표를 정해 왔는데, 이건 페이지가 1장일 때에나 가능하다. 왜냐하면 페이지가 여러장인 경우, 머리말과 꼬리말은 여러페이지에 반복해서 인쇄되어야 할 것이고, 페이지를 나눌떄의 기준점을 페이지의 마지막 좌표가 아니라 본문의 마지막 좌표를 기준으로 해야 하기 때문이다. 따라서 각 구역을 기준으로 상대좌표를 정하는게 합리적이다. ===== 구역화하기 ===== ==== 1. 절대 좌표 및 구역의 너비와 높이를 저장하는 클래스 만들기 ==== 다음과 같이 각 구역의 속성을 기억하는 클래스를 만들었다. // Rect 좌표와 너비 높이 data class PNL(private val rect : Rect = Rect(0, 0, 100, 100), private val width : Int = 0, private val height : Int = 0) { var rectP = rect var Width = width var Height = height } ==== 2. 각 구역의 절대 좌표 기억하기 ==== A4종이에서 본문을 중심으로 한 각 마진을 구한 후, 이를 기초로 하여 본문 지역의 위치 속성을 다음과 같이 지정했다. // creating a PDF Document instance val pdfDocument: PdfDocument = PdfDocument() // 여백 val marginTop = 42 val marginBottom = 50 val marginLeft = 47 val marginRight = 48 // 각 구역 좌표 var body = PNL(Rect(marginLeft, marginTop, PDF_PAGE_WIDTH - marginLeft, PDF_PAGE_HEIGHT - marginBottom), PDF_PAGE_WIDTH - (marginLeft + marginRight), PDF_PAGE_HEIGHT - (marginTop + marginBottom) ) ===== 구역화를 할 때 고려해야 할 문제점 ===== 안드로이드의 기본 PDF API(android.graphics.pdf)를 이용하여 pdf문서를 만들고자 할 때의 가장 큰 문제점은, pdf의 페이지 작성 및 캔버스 그리기는 모두 일방향이라는 것이었다. 즉, 페이지를 먼저 만들면, 그 페이지 안에 canvas가 이미 지정되어 있고, 그 캔버스 안에 그림을 그리는 방식이다. 예를 들면 다음과 같다((https://developer.android.com/reference/android/graphics/pdf/PdfDocument)). // creating a PDF Document instance val pdfDocument: PdfDocument = PdfDocument() // create a page description val myPageInfo: PdfDocument.PageInfo = PdfDocument.PageInfo.Builder(PDF_PAGE_WIDTH, PDF_PAGE_HEIGHT, num).create() // start a page val newPage: PdfDocument.Page = pdfDocument.startPage(a4Paper(pageNum + 1)) // start canvas val canvas = newPage.canvas // draw something on the page canvas.draw(....) // finish the page pdfDocument.finishPage(page); . . . // add more pages . . . // write the document content pdfDocument.writeTo(getOutputStream()); // close the document pdfDocument.close(); 위와 같이 canvas는 page에서 get하는 것만 가능하고, set은 안된다. 따라서 어떤 식으로 하든, 이미 만들어진 canvas를 pdf page에 복사할 수가 없다. 그래서 고민끝에 생각해낸 해결책은 다음의 세가지이다. 첫째, 안드로이드에서 제공하는 pdf graphics api가 아닌, 제3의 라이브러리를 이용하는 것이다. [[https://itextpdf.com/|iText]]라는 라이브러리가 있는데, 찾아보니 이 라이브러리로는 헤더와 푸터를 만들어주었다. 그런데 내가 구현하려고 하는 궁극적인 목적인 '꼬리말에 전체 페이지수와 현재페이지수를 보여주기'는 이 라이브러리로 가능한지가 모르겠다. 무엇보다도, 여기까지 왔는데 외부 라이브러리를 쓴다는건 허탈하기도 할 뿐더러, 무척 자존심이 상하는 일이기도 하다. 둘째, canvas는 비단 pdf page로만 만들 필요가 없다. canvas만 따로 선언이 가능하다. 그리고 이 경우에는 캔버스가 비트맵을 그려주었다. 이러한 비트맵그림 파일을 pdf의 본문 영역에 그림으로 그려주는 것도 가능하다. 이하에서는 이에 대하여 알아 보도록 하겠다. 다만 후술하겠지만 이 경우에는 문제가 생긴다. 셋째, 본문 영역에 대한 pdf를 만든 후에, 이 작업을 모두 기억했다가 다시 헤더와 푸터가 포함되어 있는 pdf전체를 만들 때에 기억한 작업을 그대로 반복하는 경우가 있을 수가 있다. 이렇게 해야만 pdf 파일이 벡터로 저장되어 확대했을 때 글자가 깨지지 않는다. ===== 비트맵으로 저장하고 그리기 ===== ==== 1. 비트맵 파일 배열 만들기 ==== 다음과 같이 비트맵 그림으로 저장할 배열을 만들 수 있다. // 비트맵 및 캔버스 리스트 만들기 val bitmapList : ArrayList = arrayListOf() val canvasList : ArrayList = arrayListOf() ==== 2. 페이지가 바뀔 때마다 비트맵을 새로 만들고 배열에 추가하기 ==== 이런 식으로 페이지가 바뀔 때마다 비트맵을 배열에 추가하는 것이다. // 캔버스 시작하기 fun beginCanvas() { bitmapList.add(Bitmap.createBitmap(body.Width, body.Height, Bitmap.Config.ARGB_8888)) canvasList.add(Canvas(bitmapList[0])) pageNum = 0; totalPOS = POS(0, 0) currentPOS = POS(0, 0) } // Next Page fun nextPage() { val newNum = totalPOS.Y / body.Height if (newNum > pageNum) { pageNum = newNum currentPOS.Y = 20 bitmapList.add(Bitmap.createBitmap(body.Width, body.Height, Bitmap.Config.ARGB_8888)) canvasList.add(Canvas(bitmapList[pageNum])) } } ==== 3. 그리기 함수들 ==== 각 그리기 함수에는 캔버스의 배열 번호를 지정해주면 될 것이다. 예를들면 다음과 같다. // Draw Title Text fun titleText(text : String) { nextPage() linefeedBig() canvasList[pageNum].drawText(text, (canvasList[pageNum].width / 2).toFloat(), currentPOS.Y.toFloat(), title) linefeedBig() } === 4. PDF 페이지 만들기 ==== 이렇게 만든 그림들을 canvas의 drawbitmap을 이용하여 본문 부분을 비트맵으로 그리면 된다. // PDF 페이지 만들기 fun makePDFDocument() { for (i in 0 until canvasList.size) { val newPage: PdfDocument.Page = pdfDocument.startPage(a4Paper(pageNum + 1)) val canvas = newPage.canvas drawPageHeader(canvas) canvas.drawBitmap(bitmapList[i], body.rectP.left.toFloat(), body.rectP.top.toFloat(), null) drawPageFooter(canvas, i + 1) pdfDocument.finishPage(newPage) } } ==== 5. 헤더와 푸터 ==== 참고로, 머리말과 꼬리말은 다음과 같이 만들었다. private fun drawPageHeader(canvas: Canvas) { canvas.drawText("간이 고소장(모욕)", (PDF_PAGE_WIDTH - 20).toFloat(), (marginTop - 7).toFloat(), textRight) canvas.drawLine(20f, marginTop - 3f, PDF_PAGE_WIDTH - 20f, marginTop - 3f, linePaint) } private fun drawPageFooter(canvas: Canvas, i : Int) { canvas.drawLine(20f, PDF_PAGE_HEIGHT - marginBottom + 10f, PDF_PAGE_WIDTH - 20f, PDF_PAGE_HEIGHT - marginBottom + 10f, linePaint) canvas.drawText( i.toString() + " / " + canvasList.size , (PDF_PAGE_WIDTH - 30).toFloat(), (PDF_PAGE_HEIGHT - marginBottom + 30f), textRight) canvas.drawText("고소인 : $plaintiffName", marginLeft.toFloat(), (PDF_PAGE_HEIGHT - marginBottom + 30f), cellBodyLeft) } === 6. 결론 ==== 이렇게 하면 머릿말과 꼬릿말은 pdf로 그린 것이지만, 본문 부문은 비트맵으로 만든 그림을 그래도 출력한 것이다. 그래서 머릿말과 꼬릿말은 확대를 해도 글자가 깨지지 않지만 본문 부분은 확대를 하면 깨지는 것을 볼 수 있다. ^ 비트맵으로 그린 본문 ^^ | {{:android:pdfdocument:bitmap1.jpg?400|bitmap1}} | {{:android:pdfdocument:bitmap2.jpg?400|bitmap2}} | 따라서 이러한 방식이 그다지 좋은 것은 아님을 알 수 있다.