===== 목적 ===== [[android:pdfdocument:sectioning|저번 포스트]]에서는 각 구역을 나누어 머릿말(Header)과 꼬리말(Footer)를 만들어 보았는데, 전체 페이지수를 알려면 일단 모두 그려보아야만 알 수 있으므로 불가피하게 본문 부분을 비트맵으로 저장하여 PDF Canvas에 그려주는 방식을 택하였다. 그런데 이렇게 하면 본문 부분은 비트맵에 불과하므로 확대를 하면 글자가 깨지는 문제점이 생긴다. 따라서 다음과 같이 본문 부분 역시 벡터로 그리려고 한다. ^ 본문 역시 벡터로 그려진 PDF ^^ | {{:android:pdfdocument:간이고소장_모욕_20241217_241217_133714_1.jpg?400|Vector1}} | {{android:pdfdocument:간이고소장_모욕_20241217_241217_133714_2.jpg?400|Vector2}} | ===== 명령어 모음 만들기 ===== ==== 1. 원리 ==== 지금까지 PDF로 그린 것을 명령어로 저장하여 다시 반복하려는 이유는 페이지가 여러장일 때에는 구역화(헤더와 푸터, 본문)를 할 때에전체 장수를 미리 알 수가 없기 때문이다. 즉, 한번 싹 다 그려야만 전체의 페이지수를 알 수 있기 때문에, 다시 반복하는 것이다. 그런데 지금까지 PDF를 그릴 떄에는, 정해진 사이즈의 캔버스 위에 특정한 좌표에다가 드로잉을 했다. 그리고 그 드로잉에는 인쇄될 글자들이 포함된다. 따라서 좌표와 명령어, 그리고 문구를 변수로 넘겨주면 명령어의 모음이 된다. ==== 2. 구조 ==== 따라서 명령어의 구조를 다음과 같이 만들었다. // 명령어 구조 data class COMMAND( val command : String, val page : Int, val textFirst : String, val textSecond : String, val pos : POS ) ==== 3. 명령어 집합 선언 ==== 다음과 같이 명령어 집합을 선언했다. // 명령어 집합 val commandList : ArrayList = arrayListOf() var isRecording = true isRecording을 통하여 명령어를 기록하는 스위치를 만들었다. 이에 대하여는 다음 항목에서 서술하겠다. ==== 4. isRecording을 통한 제어 ==== === 가. 제어 함수 만들기 === isRecording이라는 스위치를 통하여, 명령어를 저장할 때에 현재 상태의 변화(예 : 좌표의 변화)를 적용할지 여부를 고를 수 있게 했다. isRecording은 다음의 함수로 호출할 것이다. // Recording commannd private fun recording(recoding : Boolean) { isRecording = recoding } === 나. 내가 했던 일을 기록 === 다음과 같이 기존 그리기 함수에다가 내가 하는 그리기를 명령어 셋트로 기록하게 했다. isRecording이 필요한 이유는, 아래에서와 같이 기록할떄의 좌표 다음에도 좌표를 옮길 필요가 있을 수 있기 때문이다. // Draw Title Text fun titleText(text : String) { nextPage() linePlus(50) canvasList[pageNum].drawText(text, (canvasList[pageNum].width / 2).toFloat(), currentPOS.Y.toFloat(), title) if (isRecording) { commandList.add(COMMAND("TitleText", pageNum, text, "", currentPOS)) } linePlus(50) } 위의 코드에서는 제목그리기(titleText)를 실행한 후,. 이를 기록한 이후에도 다시 좌표를 세로로 50포인트 내리게 했다. 그리기 이후의 좌표 설정은 굳이 명령어를 기록할 떄 필요 없는 일이다. 커맨드는 첫번째 인자로 사용할 컴맨드(=TitleText), 두번쨰 인자로 현재의 페이지수, 세번쨰 인자로 넘겨줄 텍스트, 네번째 인자로는 두번째 텍스트이다(그런데 제목만 그려주는 이 경우에는 두번째 텍스트는 없다). 그리고 마지막으로 다섯번째 인자는 현재 페이지 내에서의 좌표이다. ==== 5. 두번째 PDF 문서를 그리기 ==== === 가. 새로운 그리기 함수 === 첫번째 그리기와 동일한 기능을 하는 두번째의 그리기 함수가 필요하다. 첫번째 그릴 때에는 그리면서 페이지수를 알게 된 것이지만, 두번쨰 pdf 페이지를 그릴 때에는 현재 페이지수를 알고 있으며, 이에 걸맞는 canvas가 주어지게 된다. 따라서 첫번쨰 그릴 떄에는 canvas의 배열(canvasList)을 이용하였지만 두번째에서는 canvas를 지정하여 인자로 넘겨줄 수 있다. // Draw Title Finally fun titleText(text : String, canvas: Canvas, pos : POS) { linePlus(50) canvas.drawText(text, (body.Width / 2).toFloat(), pos.Y.toFloat(), title) } 사실 두번째에 그릴 떄에는 첫번쨰 그릴때에 확정한 위치(pos)를 가져와서 canvas에 그리는 것이므로,. 그리고 난 이후에는 위치를 조정할 필요가 없다. === 나. 전체 페이지 중 본문을 그리기 === [[android:pdfdocument:sectioning|이전에구역화하기]]에서는 본문부분을 비트맵그림으로 그렸었다. 이를 다음과 같이 2번째 그리기로 바꾸었다. // PDF 페이지 만들기 fun makePDFDocument() { recording(false) for (i in 0 until canvasList.size) { val newPage: PdfDocument.Page = pdfDocument.startPage(A4) val canvas = newPage.canvas drawPageHeader(canvas) // canvas.drawBitmap(bitmapList[i], body.rectP.left.toFloat(), body.rectP.top.toFloat(), null) canvas.save() canvas.translate(marginLeft.toFloat(), marginTop.toFloat()) todoList(commandList, canvas, i) canvas.restore() drawPageFooter(canvas, i + 1) pdfDocument.finishPage(newPage) } } todoList에는 cavnas의 페이지 순서를 인자로 내보내 준다. === 다. 기록한 명령어를 호출하기 === 위에서 본문 부분에 그리기로 한 todoList는 다음과 같이 만들었다. // Command Function fun todoList(commands : ArrayList, canvas: Canvas, page : Int) { val todo = commands.filter { it.page == page } todo.forEach() { when (it.command) { "TitleText" -> titleText(it.textFirst, canvas, it.pos ) "HeaderText"-> headerText(it.textFirst, canvas, it.pos) "BodyText" -> bodyText(it.textFirst, canvas, it.pos) "BodyTextAlign" -> { when (it.textSecond) { "NORMAL" -> bodyText(it.textFirst, Layout.Alignment.ALIGN_NORMAL, canvas, it.pos) "CENTER" -> bodyText(it.textFirst, Layout.Alignment.ALIGN_CENTER, canvas, it.pos) "OPPOSITE" -> bodyText(it.textFirst, Layout.Alignment.ALIGN_OPPOSITE, canvas, it.pos) } } "BodyTextDegree" -> bodyText(it.textFirst, it.textSecond.toFloat(), canvas, it.pos) "VerticalText" -> verticalTxt(it.textFirst, canvas, it.pos) "TablePlaintiff" -> tablePlaintiff(canvas, it.pos, it.textSecond) "NoneContact" -> noneContact(canvas, it.pos) } } } todoList는 제일 처음에 각 페이지별로 filter를 한다. 만약 페이지별로 커맨드를 나누지 않으면, 동일한 페이지, 동일한 좌표에 여러개의 커맨드가 중첩적용될 수 있기 때문이다. 즉, 페이지를 구분하여 특정의 canvas에 적용하는 캔버스가 무엇인지를 명확히 했다. 그 이후에는 COMMAND배열의 첫번쨰 인자인 command항목에 따라서 특정의 명령어를 실행하도록 하였다. 이런식으로 저장한 명령어 모음을 다시 실행하도록 하였다. ===== 고소인과 피고소인 테이블 만들기 ===== ==== 1. 목적 ==== 고소인과 피고소인은 한번에 그릴 때에 여러개의 변수가 같이 넘어간다. 이를테면, 고소인1명에 대한 표를 그릴 때에는 성명, 주소, 연락처, 주민등록번호와 같은 데이터들을 한번에 그려야 하는 것이다. 따라서 이들을 JSON으로 변환하여 위의 COMMAND클래스에 2번쨰 스트링인자에 변수로 넘겨주어야 한다. ==== 2. JSON 만들기와 가져오기 ==== === 가. JSON 만들기 === 다음과 같이 고소인 연락처를 JSON 스트링으로 만드는 함수를 만들었다. // 고소인, 피고소인 연락처 JSON으로 만들기 fun setJsonContactPlaintiff(contact : PlaintiffContact) : JSONObject { val jsonContact = JSONObject() try { jsonContact.put("name", contact.pName) // 이름 jsonContact.put("socialNumber", contact.socialNumber) // 주민번호 jsonContact.put("address", contact.pAddress) // 주소 jsonContact.put("phoneNumber", contact.pPhoneNumber) // 연락처 } catch (e: Exception) { // TODO Auto-generated catch block e.printStackTrace() } return jsonContact } [[android:json파싱하기|JSON 파싱]]에 대한 보다 자세한 설명은 [[android:json파싱하기|JSON 파싱하기]] 문서를 참조하라 === 나. JSON 스트링을 변환하기 === 다음과 같이 스트링을 가져와서 변환하게 할 수 있다. // 고소인, 피고소인 연락처 가져오기 fun getJsonContactPlaintiff(jsonObject: JSONObject) : PlaintiffContact { val valueOfContact = PlaintiffContact( pName = jsonObject.optString("name", "없 음"), socialNumber = jsonObject.optString("socialNumber", "없 음"), pAddress = jsonObject.optString("address", "없 음"), pPhoneNumber = jsonObject.optString("phoneNumber", "없 음"), pPhoto = jsonObject.optString("photo", ""), ) return valueOfContact } ==== 3. JSON 보내기 ==== === 가. 코드 === 다음과이 고소인, 피고소인 주소록 명단 중에서 한명의 정보를 JSON 형태로 내보낸다. if (isRecording) { val jsonContact = setJsonContactPlaintiff(it) commandList.add(COMMAND("TablePlaintiff", pageNum, index.toString(), jsonContact.toString(), currentPOS)) } === 나. 전체 코드 === 이해를 위한 전체코드는 다음과 같다. // 표 : 고소인 fun tablePlaintiff() { nextPage() /** 변수 **/ val cellHeight = 30 // Row Height : 30 val columnWidth = 60 // 내부선 val secondColumn = 260 // 두번쨰 열 val vcOffset = 4f // 세로 중간을 맞추기 위한 오프셋 plaintiffList?.forEachIndexed() { index, it -> nextPage(cellHeight * 3) // 첫째 행 // Rect val rect1 = Rect(currentPOS.X, currentPOS.Y, body.Width, currentPOS.Y + cellHeight) canvasList[pageNum].drawRect(rect1, linePaint) // 첫쨰 줄 val rect1head1 = Rect(currentPOS.X, currentPOS.Y, currentPOS.X + columnWidth, currentPOS.Y + cellHeight) canvasList[pageNum].drawRect(rect1head1, cellFill) // 첫쨰 줄 첫쨰 제목 val rect1head2 = Rect(currentPOS.X + secondColumn, currentPOS.Y, currentPOS.X + secondColumn + columnWidth, currentPOS.Y + cellHeight) canvasList[pageNum].drawRect(rect1head2, cellFill) // 첫쨰 줄 둘쨰 제목 // Text canvasList[pageNum].drawText("이 름", rect1head1.exactCenterX(), rect1head1.exactCenterY() + vcOffset , cellHeader) canvasList[pageNum].drawText(it.pName, currentPOS.X.toFloat() + columnWidth + (secondColumn - columnWidth) / 2, currentPOS.Y.toFloat() + cellHeight / 2 + vcOffset, cellBody) canvasList[pageNum].drawText("주민번호", currentPOS.X.toFloat() + secondColumn + columnWidth / 2, currentPOS.Y.toFloat() + cellHeight / 2 + vcOffset, cellHeader) // 2번쨰 칸 canvasList[pageNum].drawText(it.socialNumber, currentPOS.X.toFloat() + secondColumn + columnWidth + (body.Width - (currentPOS.X + secondColumn + columnWidth)) / 2, currentPOS.Y.toFloat() + cellHeight / 2 + vcOffset, cellBody) // 둘재 행 // Rect canvasList[pageNum].drawRect(currentPOS.X.toFloat(), currentPOS.Y.toFloat() + cellHeight, body.Width.toFloat(), currentPOS.Y.toFloat() + cellHeight * 2, linePaint) // 둘쨰 줄 canvasList[pageNum].drawRect(currentPOS.X.toFloat(), currentPOS.Y.toFloat() + cellHeight, currentPOS.X.toFloat() + columnWidth, currentPOS.Y.toFloat() + cellHeight * 2, cellFill) // 제목박스 // Text canvasList[pageNum].drawText("주 소", currentPOS.X.toFloat() + columnWidth / 2, currentPOS.Y.toFloat() + cellHeight * 1 + cellHeight / 2 + vcOffset, cellHeader) canvasList[pageNum].drawText(it.pAddress, currentPOS.X.toFloat() + columnWidth + (body.Width - (currentPOS.X + columnWidth)) / 2, currentPOS.Y.toFloat() + cellHeight * 1 + cellHeight / 2 + vcOffset, cellBody) // 셋쨰 행 // Rect canvasList[pageNum].drawRect(currentPOS.X.toFloat(), currentPOS.Y.toFloat() + cellHeight * 2, body.Width.toFloat(), currentPOS.Y.toFloat() + cellHeight * 3, linePaint) // 셋쨰 줄 canvasList[pageNum].drawRect(currentPOS.X.toFloat(), currentPOS.Y.toFloat() + cellHeight * 2, currentPOS.X.toFloat() + columnWidth, currentPOS.Y.toFloat() + cellHeight * 3, cellFill) // 제목박스 // Text canvasList[pageNum].drawText("연 락 처", currentPOS.X.toFloat() + columnWidth / 2, currentPOS.Y.toFloat() + cellHeight * 2 + cellHeight / 2 + vcOffset, cellHeader) // 제목 canvasList[pageNum].drawText(it.pPhoneNumber, currentPOS.X.toFloat() + columnWidth + (body.Width - (currentPOS.X + columnWidth)) / 2, currentPOS.Y.toFloat() + cellHeight * 2 + cellHeight / 2 + vcOffset, cellBody) // 내용 if (isRecording) { val jsonContact = setJsonContactPlaintiff(it) commandList.add(COMMAND("TablePlaintiff", pageNum, index.toString(), jsonContact.toString(), currentPOS)) } // 아래 여백 linePlus(cellHeight * 3 + 20) }?:{ canvasList[pageNum].drawRect(currentPOS.X.toFloat(), currentPOS.Y.toFloat(), body.Width.toFloat(), currentPOS.Y.toFloat() + cellHeight, linePaint) // 첫쨰 줄 canvasList[pageNum].drawText("당사자 정보가 없습니다. 당사자를 추가했는지 확인하세요!", currentPOS.X.toFloat() + body.Width / 2, currentPOS.Y.toFloat() + cellHeight / 2 + vcOffset , cellHeader) if (isRecording) { commandList.add(COMMAND("NoneContact", pageNum, "", "", currentPOS)) } // 아래 여백 linefeed() } } ==== 4. JSON 가져오기 ==== JSON으로 가져온 스트링 데이터를 다시 나누어서 표로 그리는건 다음과 같이 하면 된다. fun tablePlaintiff(canvas: Canvas, pos: POS, jsonObject: String) { /** 변수 **/ val cellHeight = 30 // Row Height : 30 val columnWidth = 60 // 내부선 val secondColumn = 260 // 두번쨰 열 val vcOffset = 4f // 세로 중간을 맞추기 위한 오프셋 val contact = getJsonContactPlaintiff(JSONObject(jsonObject)) // 첫째 행 // Rect val rect1 = Rect(pos.X, pos.Y, body.Width, pos.Y + cellHeight) canvas.drawRect(rect1, linePaint) // 첫쨰 줄 val rect1head1 = Rect(pos.X, pos.Y, pos.X + columnWidth, pos.Y + cellHeight) canvas.drawRect(rect1head1, cellFill) // 첫쨰 줄 첫쨰 제목 val rect1head2 = Rect(pos.X + secondColumn, pos.Y, pos.X + secondColumn + columnWidth, pos.Y + cellHeight) canvas.drawRect(rect1head2, cellFill) // 첫쨰 줄 둘쨰 제목 // Text canvas.drawText("이 름", rect1head1.exactCenterX(), rect1head1.exactCenterY() + vcOffset , cellHeader) canvas.drawText(contact.pName, pos.X.toFloat() + columnWidth + (secondColumn - columnWidth) / 2, pos.Y.toFloat() + cellHeight / 2 + vcOffset, cellBody) canvas.drawText("주민번호", pos.X.toFloat() + secondColumn + columnWidth / 2, pos.Y.toFloat() + cellHeight / 2 + vcOffset, cellHeader) // 2번쨰 칸 canvas.drawText(contact.socialNumber, pos.X.toFloat() + secondColumn + columnWidth + (body.Width - (pos.X + secondColumn + columnWidth)) / 2, pos.Y.toFloat() + cellHeight / 2 + vcOffset, cellBody) // 둘재 행 // Rect canvas.drawRect(pos.X.toFloat(), pos.Y.toFloat() + cellHeight, body.Width.toFloat(), pos.Y.toFloat() + cellHeight * 2, linePaint) // 둘쨰 줄 canvas.drawRect(pos.X.toFloat(), pos.Y.toFloat() + cellHeight, pos.X.toFloat() + columnWidth, pos.Y.toFloat() + cellHeight * 2, cellFill) // 제목박스 // Text canvas.drawText("주 소", pos.X.toFloat() + columnWidth / 2, pos.Y.toFloat() + cellHeight * 1 + cellHeight / 2 + vcOffset, cellHeader) canvas.drawText(contact.pAddress, pos.X.toFloat() + columnWidth + (body.Width - (pos.X + columnWidth)) / 2, pos.Y.toFloat() + cellHeight * 1 + cellHeight / 2 + vcOffset, cellBody) // 셋쨰 행 // Rect canvas.drawRect(pos.X.toFloat(), pos.Y.toFloat() + cellHeight * 2, body.Width.toFloat(), pos.Y.toFloat() + cellHeight * 3, linePaint) // 셋쨰 줄 canvas.drawRect(pos.X.toFloat(), pos.Y.toFloat() + cellHeight * 2, pos.X.toFloat() + columnWidth, pos.Y.toFloat() + cellHeight * 3, cellFill) // 제목박스 // Text canvas.drawText("연 락 처", pos.X.toFloat() + columnWidth / 2, pos.Y.toFloat() + cellHeight * 2 + cellHeight / 2 + vcOffset, cellHeader) // 제목 canvas.drawText(contact.pPhoneNumber, pos.X.toFloat() + columnWidth + (body.Width - (pos.X + columnWidth)) / 2, pos.Y.toFloat() + cellHeight * 2 + cellHeight / 2 + vcOffset, cellBody) // 내용 } // 명단이 없을 떄 fun noneContact(canvas: Canvas, pos: POS) { /** 변수 **/ val cellHeight = 30 // Row Height : 30 val vcOffset = 4f // 세로 중간을 맞추기 위한 오프셋 canvas.drawRect(pos.X.toFloat(), pos.Y.toFloat(), body.Width.toFloat(), pos.Y.toFloat() + cellHeight, linePaint) // 첫쨰 줄 canvas.drawText("당사자 정보가 없습니다. 당사자를 추가했는지 확인하세요!", pos.X.toFloat() + body.Width / 2, pos.Y.toFloat() + cellHeight / 2 + vcOffset , cellHeader) } ===== 여러개의 정보를 스트링으로 넘기기 ===== ==== 1. 텍스트 합치기와 나누기 ==== === 가. 텍스트 합치기 === 여러 줄의 데이터를 하나의 텍스트로 합치려면 다음과 같이 하면 될 것이다. 이 때 구분자는 세미콜론(;)으로 하였다. var totalStr = "" bodyStr.forEach (){ if (totalStr != "") totalStr += ";" totalStr += it } === 나. 텍스트 나누기 === 텍스트를 나누는 함수는 kotlin에서 split함수로 제공한다. 다음과 같이 하면 된다. val wordsBits = bodyStr.split(";") val line = wordsBits.size ==== 2. 텍스트 합쳐서 데이터로 전송해주기 ==== 처음에는 다음과 같이 데이터를 뿌려준 다음 이를, command 모음에 다음과 같이 전송해준다. // MultiLine Table Text fun tableText(header : String, bodyStr : MutableList) { /** 변수 **/ val cellHeight = 20 // Row Height : 40 val columnWidth = 180f // 내부선 val vcOffset = 4f // 세로 중간을 맞추기 위한 오프셋 val line = bodyStr.size nextPage(cellHeight * line + 20) // Rect canvasList[pageNum].drawRect(currentPOS.X.toFloat(), currentPOS.Y.toFloat(), body.Width.toFloat(), currentPOS.Y.toFloat() + line * cellHeight + 20, linePaint) // 첫쨰 줄 canvasList[pageNum].drawRect(currentPOS.X.toFloat(), currentPOS.Y.toFloat(), currentPOS.X.toFloat() + columnWidth, currentPOS.Y.toFloat() + line * cellHeight + 20, linePaint) // 제목박스 // Text canvasList[pageNum].drawText(header, currentPOS.X.toFloat() + columnWidth / 2, currentPOS.Y.toFloat() + (line * cellHeight + 20) / 2 + vcOffset, cellHeaderLight) // 칸 내에 여러줄의 글을 쓰기 for (i : Int in 0 until line ) { canvasList[pageNum].drawText(bodyStr[i], currentPOS.X.toFloat() + columnWidth + 10, currentPOS.Y.toFloat() + cellHeight * (i) + 10 + vcOffset, cellBodyLeft) } var totalStr = "" bodyStr.forEach (){ if (totalStr != "") totalStr += ";" totalStr += it } if (isRecording) { commandList.add(COMMAND("TableTextMultiLine", pageNum, header, totalStr, currentPOS)) } // 아래 여백 totalPOS += POS(0, cellHeight * line + 20) currentPOS += POS(0, cellHeight * line + 20) } ==== 3. 2차 그리기에서 텍스트 나눠서 다시 그리기 ==== 이렇게 묶음으로 받은 텍스트를 나눠서 다시 뿌려주면 된다. // Table Text 2nd paint fun tableTextMultiLine(header : String, bodyStr : String, canvas: Canvas, pos: POS) { /** 변수 **/ val cellHeight = 20 // Row Height : 40 val columnWidth = 180f // 내부선 val vcOffset = 4f // 세로 중간을 맞추기 위한 오프셋 val wordsBits = bodyStr.split(";") val line = wordsBits.size // Rect canvas.drawRect(pos.X.toFloat(), pos.Y.toFloat(), body.Width.toFloat(), pos.Y.toFloat() + line * cellHeight + 20, linePaint) // 첫쨰 줄 canvas.drawRect(pos.X.toFloat(), pos.Y.toFloat(), pos.X.toFloat() + columnWidth, pos.Y.toFloat() + line * cellHeight + 20, linePaint) // 제목박스 // Text canvas.drawText(header, pos.X.toFloat() + columnWidth / 2, pos.Y.toFloat() + (line * cellHeight + 20) / 2 + vcOffset, cellHeaderLight) // 칸 내에 여러줄의 글을 쓰기 for (i : Int in 0 until line ) { canvas.drawText(wordsBits[i], pos.X.toFloat() + columnWidth + 10, pos.Y.toFloat() + cellHeight * (i) + 20 + vcOffset, cellBodyLeft) } } ===== 최종 결과물 ===== 이렇게 PDF 2차로 그리면 본문과 머릿말 꼬릿말을 모두 벡터형식으로 그릴 수가 있다. 최종 결과물은 다음과 같다. 실제 PDF로 파일을 다운 받아 보면, 스케일을 키워도 글자가 깨지지 않고 자연스럽게 크기가 커지는 것을 볼 수 있다. ^ 벡터로 저장된 PDF 문서 ^^ | {{android:pdfdocument:간이고소장_모욕_20241224_241224_195111_1.jpg?400|vector1}} | {{android:pdfdocument:간이고소장_모욕_20241224_241224_195111_2.jpg?400|vector2}} |