- 전체
- 명
- 오늘 찾아주신 분
- 명
이전 포스팅 에서는 Glide 와 Pdfium 라이브러리를 앱에서 사용하기 위한 방법에 대해 간단하게 알아보았다. 못보고 왔다면 조회수 올리게 한번 보고 오세용
Glide with PDF -1(기초공사)-
Glide 는 Android 에서는 거의 필수적으로 사용하는 이미지 로드 라이브러리이다. 뭐 이보다 Lightweight 하다는 Picasso 도 유명한 라이브러리 이지만, Glide 가 그나마 접근하기 쉬운 라이브러리이며 구
tempodivalse.tistory.com
이전 꺼는 그냥 어느 정도 보면 대충 만드는 정도지만 여기서는 직접적인 구현이 들어가서 (영어문서를) 이해하는 데 조금 어려웠다. 그럼 시작해보도록 하자.
Glide 문서를 보면 Writing a custom ModelLoader 라는 섹션을 찾아볼 수 있는데, 해당 문서가 너무 잘 되어있어서 순서대로 따라 만들수 있게 되어있다. 완벽한 번역은 나도 네이티브 스피커가 아니기 때문에 부분부분 이해를 할 수 있었지만, 대략의 순서는 다음과 같다
1. ModelLoader 를 구현한다
2. DataFetcher 를 구현한다.
3. ModelLoader 에서 쓸 수 있도록 만든다
4. AppGlideModule 에 등록한다.
이렇게 4개의 섹션으로 되어있지만 막상 구현하라고 하면 어떻게 할 지 모르는데 다행히도 Base64 인코딩 된 데이터를 ModelLoader, DataFetcher 로 만드는 예시가 있어서 그것을 참고하여 만들 수 있었다. 하지만 우리는 PDF 관련 Loader 를 구현해야 하니까 하나씩 공식문서를 토대로 만들어 보도록 하자.
1. ModelLoader 구현
Glide 문서에 따르면 ModelLoader 에서 다룰 수 있는 스트림 객체는 InputStream 과 ByteBuffer, ParcelFileDescriptor 세가지를 기본적으로 지원하고 있다고 말하고 있는 "것 같다." 여기서 ParcelFileDescriptor 는 동영상 객체의 Frame 을 렌더시키기 위해 지원 해주고 있다고 얘기 한다.
이 중에서 나는 ByteBuffer 를 전달하여 로드 해보려 한다. 왜냐면 가이드 문서에도 ByteBuffer 를 사용하고 있기 때문이다. 로드할 때 사용하는 Model 객체는 Uri, File, String 등등.. 사용할 수 있지만, 여기서는 Uri 를 이용해보돍 한다.
ModelLoader 는 Generic 이기 때문에 이를 상속하게 되면 첫번째 타입은 Model 의 객체가 될 것이고, 두번째 타입은 변환할 Data 타입이 될 것이다. 그래서 ModelLoader <Uri, ByteBuffer> 로 해서 상속을 받게 된다면 다음과 같은 메소드들이 자동 입력된다.
사진 내에도 주석으로 적어놓았지만,
- buildLoadData
- handles
이 두가지 메소드들을 오버라이드 하도록 입력해준다.
먼저 가장 알기 쉬운 handles() 메소드를 알아보자면, 객체들 중에서 어떤 객체를 이 ModelLoader 에서 다룰 것인지 필터링 해주는 메소드라고 할 수 있다. 우리는 Uri 를 이용하여 PDF 객체를 다룰 것이기 context 의 contentResolver 를 이용하여 mimeType을 가져올 것이다.
override fun handles(model: Uri): Boolean
= context.contentResolver.getType(uri) == "application/pdf"
만약 File 객체를 사용하려 한다면,
override fun handles(model: File): Boolean = model.extension == "pdf"
webkit 의 MimeTypeMap 을 사용해서 판별할 수 있을 것이다.
override fun handles(model: File): Boolean
= MimeTypeMap.getSingleton().getMimeTypeFromExtension(model.extension) == "application/pdf"
다음으로 buildLoadData 는, DataFetcher 에서 가져온 객체를 ModelLoader.LoadData 형태로 전달해줘야 한다. 여기에서 ModelLoader.LoadData 의 Constructor 형태는 Cache Key 그리고 Fetcher 순서로 Parameter 를 받고 있다. 그래서 리턴하는 형태를 간략히 보면,
override fun buildLoadData(model: Uri, width: Int, height: Int, options: Options)
: ModelLoader.LoadData<ByteBuffer>? {
return ModelLoader.LoadData<ByteBuffer>(CACHE_KEY, FETCHER)
}
이런 식으로 받아야 하기 때문에 DataFetcher 가 구현이 먼저 선행되어야 가능하다.
2. DataFetcher 구현
DataFetcher 에서는 실질적으로 Uri 에서 추출한 데이터를 ByteBuffer 로 변환하여 내보내는 역할을 하는 클래스 이다.
DataFetcher 도 Generic 형태인데 파라미터는 ModelLoader 로 리턴할 데이터 형태를 넣어주면 된다. 그러니까 ModelLoader Generic 의 두번째 타입을 넣어주면 된다는 것이다. 그리고나서 override 할 것들을 자동으로 불러오면 총 5개의 메소드를 override 하게 된다.
- loadData
- cleanup
- cancel
- getDataClass
- getDataSource
먼저 제일 하단의 getDataSource() 는, DataSource 의 Cache Policy 에 대해 정의를 하는 것 같은데 Preset 되어있어서 크게 구현하지 않아도 된다. 어떤 목적으로 사용하는 지는 잘 모르지만, Local 데이터를 불러오는 것에는 Local 로 맞추고 Http 에서 불러오는 것에서는 Remote 로 맞추라는 공식문서의 설명이 있기에 해당 메소드는 다음과 같이 맞추도록 한다.
override fun getDataSource(): DataSource = DataSource.LOCAL
그 외에도 DataSource 에는 MEMORY_CACHE, DATA_DISK_CACHE, RESOURCE_DISK_CACHE 등이 있는데 이것들은 어떤 역할을 하는 지 정확하게 파악하지 못했으니 파악하면 수정하는걸로.
다음, getDataClass() 는 단순하게 리턴하는 타입의 클래스객체를 리턴해주면 된다. 그래서
override fun getDataClass(): Class<ByteBuffer> = ByteBuffer::class.java
이렇게만 해주면 끝난다
다음, cancel() 은 Http 요청을 하는 모델인 경우에만 사용한다고 나와있고, cancellation 하는 API 가 있는 경우에 해당 메소드에서 호출하도록 구현하라고 써있다. 그래서 나는 해당 구역은 패스하도록 했다.
다음, cleanup() 은 I/O 가 일어난 모델인 경우에는 해당 메소드안에서 release 하는 방식을 구현하라고 문서에는 나와있다. 이 부분은 loadData와 함께 구현하는 것이 맞기 때문에 잠시 Pending.
마지막, loadData() 이 메소드에서 Uri 를 통해 데이터를 추출하여 ByteBuffer 로 담아내는 일을 하게 된다. 그렇다는 것은 PDF 를 로드하는 것을 이 부분에서 해야 한다는 이야기가 되는 것이다.
PDF 를 로드하는 라이브러리에 대해서는 이전에 Pdfium 을 사용하면 되는데, Pdfium 은 FileDescriptor 객체를 이용하여 PDF 를 네이티브 소스에서 열게 된다.
이래저래 하고 여차저차 하게 되어 ByteBuffer 로 데이터를 변환할 수 있게 되었다면 loadData 의 callback 객체 내에 있는 onDataReady() 메소드에 만들어진 ByteBuffer 를 던진다. 하지만 혹시나 PDF 처리를 하다가 에러가 발생할 수도 있다. 그런 경우를 위해 callback 객체 내에는 onLoadFailed() 라는 객체를 만들어 놓았다.
그래서 간략한 형태를 보면,
override fun loadData(priority: Priority, callback
: DataFetcher.DataCallback<in ByteBuffer>) {
try {
/* 신나게 코드를 짠다. 막 짠다. */
callback.onDataReady(BYTE_BUFFER)
} catch (e : Exception) {
callback.onLoadFailed(e)
}
}
이런 식으로 분기처리 하도록 한다. 해당 callback 은 Glide 의 Chain 메소드인 addListener() 의 object 에 들어가게 된다.
DataFetcher 까지 만들었다면 다시 ModelLoader 로 돌아가 미처 구현하지 못한 곳을 구현해본다.
3. ModelLoader 에서 쓸 수 있도록 만든다
ModelLoader 부분에서 언급한 내용을 보면,
다음으로 buildLoadData 는, DataFetcher 에서 가져온 객체를 ModelLoader.LoadData 형태로 전달해줘야 한다. 여기에서 ModelLoader.LoadData 의 Constructor 형태는 Cache Key 그리고 Fetcher 순서로 Parameter 를 받고 있다.
라고 했는데, 이제 Fetcher 객체는 DataFetcher 가 구현되었으니 완성이 되었고 Cache Key 를 만들어 볼 차례이다.
Glide 에서 다뤄지는 Cache Key 는 Key 라고 하는 Interface 를 상속받아 구현하는데, 따로 그럴 필요도 없이 Glide 에서 제공하는 ObjectKey 객체를 이용하여 만들면 된다. 그래서,
ObjectKey("PAGE_${model}_")
이런 식으로 만들면 되는데, 이것을 바로 fetcher 객체와 묶어 보내면 문제되는 것이 있다. 바로 Page 별로 공통 Cache Key 가 생성된다는 것이다. 그래도 페이지 별로 Caching 이 먹혀야 Glide 를 쓰는 목적이 맞는 건데 Cache Target 이 똑같아 버리면 페이지를 여러 개 불러온다 해도 모든 View 에서는 마지막 PDF 페이지만 불러오게 되는 대참사를 겪게 되기 때문에 Page 별로 다른 Key 를 가지도록 만들어야 한다.
그래서 CacheKey 는,
ObjectKey("PAGE_${model}_${page}")
이렇게 되어야 하는 것이 맞는 것이다. 그런데, 정작 메소드 내에서는 page 정보를 가져올 방법이 없다는 것이다. 그러면 어떻게 하느냐.. 해서 메소드의 파라미터를 보면 Options 객체가 있는 것이다. 우리는 이 Options 객체를 가지고 Page 정보를 가져올 수 있도록 로직을 만들 것이다.
Option 객체는 Glide 객체 내에서 유일한 키 값을 가지고 있어야 하고, static 의 영역에서 선언이 되어야 하는 조건이 있다. 그래서 Kotlin 에서의 Static 영역인 Companion 에 Option 을 선언해 주도록 한다.
companion object {
private const val OPTION_KEY_PAGE = "glide.pdf.page"
val OPTION_PAGE : Option<Int> = Option.memory(OPTION_KEY_PAGE, 0)
}
Key 값은 Unique 하게 지어주면 되기 떄문에 아무렇게나 식별 가능하게만 지어준다. 그렇다고 저걸 이용하여 Value 를 뽑아내는 방식이 아닌 다른 방식을 사용하기 때문에 그냥 지정만 해놓는다. 왜냐하면 Option<*> 으로 선언된 객체 자체가 Key 가 되기 때문이다.
Option 객체에는 memory(), disk() 두 가지의 정의 방법이 있는데 둘의 차이점은 해석이 모호한 관계로 그냥 감만 잡아 개발 하였고, disk cache 를 쓰지 않는 조건으로 개발 했기 때문에 memory() 메소드를 이용하여 간단하게 개발했다.
만약 disk() 메소드를 이용하여 개발하게 된다면 다음과 같이 복잡한 개발이 이뤄진다. 처음에는 이 방식으로 개발 해봤는데 해당 내용은 Glide 에서 사용하고 있는 방식을 보고 바꾼 내용이라 무슨 작동 방식인 지는 잘 이해되지 않는다.
val OPTION_PAGE : Option<Int> = Option.disk(OPTION_KEY_PAGE, 0, object :
Option.CacheKeyUpdater<Int> {
private val buffer = ByteBuffer.allocate(Int.SIZE_BITS / Byte.SIZE_BITS)
override fun update(keyBytes: ByteArray, value: Int, messageDigest: MessageDigest) {
val _v = value.coerceAtLeast(0)
messageDigest.update(keyBytes)
synchronized(buffer){
buffer.position(0)
messageDigest.update(buffer.putInt(_v).array())
}
}
})
대략 Buffer 할당해서 Buffer 내용을 disk 에 할당할 것 같은 분위기였다. 이번 개발에서는 memory() 방식으로 했는데 이것도 잘 되가지고 간단한 것으로 해보도록 했다. 이제 옵션도 정의 했으니, Glide 객체에 Option 을 달아보도록 하자.
Glide 는 Chaining 패턴을 가지고 있기 때문에 이것을 유지하기 위해서는 Kotlin 의 extension 기능을 사용하여 추가를 해보았다. 그래서
/* GlideExtensions.kt */
fun GlideRequest<Bitmap>.page(page: Int) = apply {
set(PDFModelLoader.OPTION_PAGE, page)
}
extension 파일만 만들어놓고 Global 하게 사용할 수 있도록 만들어놓았고, Chaining 이 끊기지 않도록 GlideRequest 를 이용하도록 했다. 어차피 Bitmap 을 가져올 거라 Bitmap 으로 타입을 박아놓았다.
그리고 ModelLoader 에도 Page 를 받을 수 있도록 변경한다.
override fun buildLoadData(model: Uri, width: Int, height: Int, options: Options)
: ModelLoader.LoadData<ByteBuffer>? {
val page = options[OPTION_PAGE]
val key = ObjectKey("PAGE_${model}_${page}")
/* Data Fetcher 를 정의해 본다 */
return ModelLoader.LoadData(key, DATA_FETCHER)
}
그러고 나서 구현한 DataFetcher에 Page 를 받을 수 있도록 하면 각 페이지 별로 렌더를 시킬 수 있고, Cache Key 까지 각 페이지별로 발급을 해줄 수 있게 되는 것이다.
다음 포스팅에서는 Glide 에 지금까지 만들어놓은 ModelLoader 를 등록해보는 포스팅을 올려보도록 하겠다.
[Android] Web 에서 App 으로 데이터를 받아보자 (0) | 2022.02.04 |
---|---|
[Android] Glide with PDF -3(완결)- (0) | 2022.02.04 |
[Android] Glide with PDF -1(기초공사)- (0) | 2022.01.26 |
[Kotlin] 문자열 배열 한글, 영문, 숫자로 정렬 시키기 (0) | 2022.01.26 |
[Android] Onedrive SDK 적용기 (6) | 2022.01.25 |