Skip to content

Commit 184ec7a

Browse files
committed
fix: improve render performance on dynamic annotation changes
1 parent 1fbf05d commit 184ec7a

3 files changed

Lines changed: 155 additions & 108 deletions

File tree

android/src/main/java/com/alpha0010/pdf/PdfView.kt

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ class PdfView(
5656
private var mAnnotation = emptyList<AnnotationPage>()
5757
private val mBitmaps = MutableList(SLICES) { createBitmap(1, 1) }
5858
private var mDirty = false
59+
private var mAnnotationDirty = false
5960
private var mPage = 0
6061
private var mPageMeasure = Size(1, 1)
6162
private var mResizeMode = ResizeMode.CONTAIN
@@ -132,7 +133,7 @@ class PdfView(
132133
if (source.isEmpty()) {
133134
if (mAnnotation.isNotEmpty()) {
134135
mAnnotation = emptyList()
135-
mDirty = true
136+
mAnnotationDirty = true
136137
}
137138
return
138139
}
@@ -143,7 +144,7 @@ class PdfView(
143144
} else {
144145
Json.decodeFromString(source)
145146
}
146-
mDirty = true
147+
mAnnotationDirty = true
147148
} catch (e: Exception) {
148149
onError("Failed to load annotation from '$source'. ${e.message}")
149150
}
@@ -228,13 +229,12 @@ class PdfView(
228229
}
229230
}
230231

231-
private fun renderAnnotation(bitmap: Bitmap) {
232+
private fun renderAnnotation(ctx: Canvas) {
232233
if (mAnnotation.size <= mPage) {
233234
// No annotation data for current page.
234235
return
235236
}
236237
val metrics = resources.displayMetrics
237-
val ctx = Canvas(bitmap)
238238
val paint = Paint()
239239

240240
// Draw strokes.
@@ -248,7 +248,7 @@ class PdfView(
248248
}
249249
paint.color = parseColor(stroke.color)
250250
paint.strokeWidth = TypedValue.applyDimension(COMPLEX_UNIT_DIP, stroke.width, metrics)
251-
ctx.drawPath(computePath(stroke.path, bitmap.width, bitmap.height), paint)
251+
ctx.drawPath(computePath(stroke.path, ctx.width, ctx.height), paint)
252252
}
253253

254254
// Draw text.
@@ -260,24 +260,34 @@ class PdfView(
260260
for (msg in mAnnotation[mPage].text) {
261261
paint.color = parseColor(msg.color)
262262
// Increase the font for larger views, but do so at a reduced rate.
263-
val scaledFont = 9 + (msg.fontSize * bitmap.width) / factor
263+
val scaledFont = 9 + (msg.fontSize * ctx.width) / factor
264264
paint.textSize = TypedValue.applyDimension(COMPLEX_UNIT_DIP, scaledFont, metrics)
265265
paint.getTextBounds(msg.str, 0, msg.str.length, bounds)
266266
ctx.drawText(
267267
msg.str,
268-
bitmap.width * msg.point[0],
269-
bitmap.height * msg.point[1] - bounds.top,
268+
ctx.width * msg.point[0],
269+
ctx.height * msg.point[1] - bounds.top,
270270
paint
271271
)
272272
}
273273
}
274274

275275
fun renderPdf() {
276-
if (height < 1 || width < 1 || mSource.isEmpty() || !mDirty) {
276+
if (height < 1 || width < 1 || mSource.isEmpty()) {
277277
// View layout not yet complete, or nothing to render.
278278
return
279279
}
280+
if (!mDirty) {
281+
if (mAnnotationDirty) {
282+
// Only annotations changed, keep cached pdf render.
283+
postInvalidate()
284+
}
285+
mAnnotationDirty = false
286+
return
287+
}
288+
280289
mDirty = false
290+
mAnnotationDirty = false
281291

282292
CoroutineScope(Dispatchers.Main).launch(Dispatchers.IO) {
283293
val file = File(mSource)
@@ -345,8 +355,6 @@ class PdfView(
345355
}
346356
fd.close()
347357

348-
renderAnnotation(bitmap)
349-
350358
withContext(Dispatchers.Main) {
351359
// Post new bitmap for display.
352360
val sliceHeight = floor(bitmap.height.toFloat() / SLICES).toInt()
@@ -425,6 +433,7 @@ class PdfView(
425433
canvas.drawBitmap(bitmap, null, viewRect, null)
426434
}
427435
}
436+
renderAnnotation(canvas)
428437
}
429438

430439
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {

ios/AnnotationView.swift

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
class AnnotationView: UIView {
2+
private var annotationData = [AnnotationPage]()
3+
private var page = 0
4+
5+
public override func draw(_ rect: CGRect) {
6+
if let context = UIGraphicsGetCurrentContext() {
7+
context.saveGState()
8+
context.clip(to: rect)
9+
self.renderAnnotation(context, scaleX: frame.width, scaleY: frame.height)
10+
context.restoreGState()
11+
}
12+
}
13+
14+
public func setAnnotationData(_ data: [AnnotationPage]){
15+
if data.isEmpty {
16+
if !annotationData.isEmpty {
17+
annotationData.removeAll()
18+
DispatchQueue.main.async { self.setNeedsDisplay() }
19+
}
20+
} else {
21+
annotationData = data
22+
DispatchQueue.main.async { self.setNeedsDisplay() }
23+
}
24+
}
25+
26+
public func setPage(_ pg: Int) {
27+
if page != pg {
28+
page = pg
29+
DispatchQueue.main.async { self.setNeedsDisplay() }
30+
}
31+
}
32+
33+
private func parseColor(_ hex: String) -> UIColor {
34+
// Parse HTML hex color. Assumes leading `#`.
35+
guard let colorInt = UInt64(hex.dropFirst().prefix(6), radix: 16) else {
36+
return UIColor.black
37+
}
38+
var alpha = CGFloat(1.0)
39+
if hex.count == 9, let alphaInt = UInt64(hex.suffix(2), radix: 16) {
40+
// Extract alpha channel.
41+
alpha = CGFloat(alphaInt) / 255.0
42+
}
43+
return UIColor(
44+
red: CGFloat((colorInt & 0xFF0000) >> 16) / 255.0,
45+
green: CGFloat((colorInt & 0x00FF00) >> 8) / 255.0,
46+
blue: CGFloat(colorInt & 0x0000FF) / 255.0,
47+
alpha: alpha
48+
)
49+
}
50+
51+
private func makeCGPoint(_ point: [CGFloat], _ scaleX: CGFloat, _ scaleY: CGFloat) -> CGPoint {
52+
return CGPoint(x: scaleX * point[0], y: scaleY * point[1])
53+
}
54+
55+
private func computeDist(_ a: [CGFloat], _ b: [CGFloat], scaleX: CGFloat, scaleY: CGFloat) -> CGFloat {
56+
return hypot(scaleX * (a[0] - b[0]), scaleY * (a[1] - b[1]))
57+
}
58+
59+
private func computePath(_ context: CGContext, _ coordinates: [[CGFloat]], scaleX: CGFloat, scaleY: CGFloat) {
60+
// Start path at the first point.
61+
var prevPoint = coordinates[0]
62+
context.move(to: makeCGPoint(prevPoint, scaleX, scaleY))
63+
for point in coordinates.dropFirst() {
64+
guard computeDist(prevPoint, point, scaleX: scaleX, scaleY: scaleY) > 3 else {
65+
// Smooth small irregularities.
66+
continue
67+
}
68+
let midX = (prevPoint[0] + point[0]) / 2
69+
let midY = (prevPoint[1] + point[1]) / 2
70+
// Draw line to the midpoint between the next two points. Use the first
71+
// point as curve control (line will bend toward it).
72+
context.addQuadCurve(
73+
to: makeCGPoint([midX, midY], scaleX, scaleY),
74+
control: makeCGPoint(prevPoint, scaleX, scaleY)
75+
)
76+
prevPoint = point
77+
}
78+
// Draw line to the last point.
79+
prevPoint = coordinates.last!
80+
context.addLine(to: makeCGPoint(prevPoint, scaleX, scaleY))
81+
}
82+
83+
private func renderAnnotation(_ context: CGContext, scaleX: CGFloat, scaleY: CGFloat) {
84+
guard page < annotationData.count else {
85+
// No annotation data for current page.
86+
return
87+
}
88+
let annotationPage = annotationData[page]
89+
90+
// Draw strokes.
91+
context.setLineCap(.round)
92+
context.setLineJoin(.round)
93+
for stroke in annotationPage.strokes {
94+
guard stroke.path.count > 1 else {
95+
continue
96+
}
97+
context.setStrokeColor(parseColor(stroke.color).cgColor)
98+
context.setLineWidth(stroke.width)
99+
100+
context.beginPath()
101+
computePath(context, stroke.path, scaleX: scaleX, scaleY: scaleY)
102+
context.strokePath()
103+
}
104+
105+
// Draw text.
106+
for msg in annotationPage.text {
107+
// Increase the font for larger views, but do so at a reduced rate.
108+
let scaledFont = 9 + (msg.fontSize * scaleX) / 1000
109+
msg.str.draw(
110+
at: makeCGPoint(msg.point, scaleX, scaleY),
111+
withAttributes: [
112+
.font: UIFont.systemFont(ofSize: scaledFont),
113+
.foregroundColor: parseColor(msg.color)
114+
]
115+
)
116+
}
117+
}
118+
}

ios/PdfView.swift

Lines changed: 17 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,21 @@ public class PdfView: UIView {
1919
@objc public var onPdfLoadComplete: PdfPageSizeHandler?
2020
@objc public var onPdfMeasure: PdfPageSizeHandler?
2121

22-
private var annotationData = [AnnotationPage]()
22+
private let annotLayer: AnnotationView
2323
private var previousBounds: CGRect = .zero
2424

25+
public override init(frame: CGRect) {
26+
annotLayer = AnnotationView()
27+
super.init(frame: frame)
28+
annotLayer.autoresizingMask = [.flexibleWidth, .flexibleHeight]
29+
annotLayer.isOpaque = false
30+
addSubview(annotLayer)
31+
}
32+
33+
required init?(coder: NSCoder) {
34+
fatalError("init(coder:) has not been implemented")
35+
}
36+
2537
@objc public func measurePdf() {
2638
guard !source.isEmpty, let dispatcher = onPdfMeasure else {
2739
return
@@ -59,16 +71,15 @@ public class PdfView: UIView {
5971
var needsMeasure = false
6072
if annotation != annot {
6173
annotation = annot
62-
isDirty = true
6374
}
6475
if annotationStr != annotStr {
6576
annotationStr = annotStr
66-
isDirty = true
6777
}
6878
if page != pg {
6979
page = pg
7080
isDirty = true
7181
needsMeasure = true
82+
annotLayer.setPage(pg)
7283
}
7384
if resizeMode != rsMd {
7485
resizeMode = rsMd
@@ -91,15 +102,14 @@ public class PdfView: UIView {
91102
if bounds != previousBounds {
92103
renderPdf()
93104
previousBounds = bounds
105+
annotLayer.setNeedsDisplay()
94106
}
95107
super.layoutSubviews()
96108
}
97109

98110
private func loadAnnotation(file: Bool) {
99111
guard !annotation.isEmpty || !annotationStr.isEmpty else {
100-
if !annotationData.isEmpty {
101-
annotationData.removeAll()
102-
}
112+
annotLayer.setAnnotationData([])
103113
return
104114
}
105115

@@ -111,7 +121,7 @@ public class PdfView: UIView {
111121
} else {
112122
data = annotationStr.data(using: .utf8)!;
113123
}
114-
annotationData = try decoder.decode([AnnotationPage].self, from: data)
124+
annotLayer.setAnnotationData(try decoder.decode([AnnotationPage].self, from: data))
115125
} catch {
116126
dispatchOnError(
117127
message: "Failed to load annotation from '\(annotation)'. \(error.localizedDescription)"
@@ -120,92 +130,6 @@ public class PdfView: UIView {
120130
}
121131
}
122132

123-
private func parseColor(_ hex: String) -> UIColor {
124-
// Parse HTML hex color. Assumes leading `#`.
125-
guard let colorInt = UInt64(hex.dropFirst().prefix(6), radix: 16) else {
126-
return UIColor.black
127-
}
128-
var alpha = CGFloat(1.0)
129-
if hex.count == 9, let alphaInt = UInt64(hex.suffix(2), radix: 16) {
130-
// Extract alpha channel.
131-
alpha = CGFloat(alphaInt) / 255.0
132-
}
133-
return UIColor(
134-
red: CGFloat((colorInt & 0xFF0000) >> 16) / 255.0,
135-
green: CGFloat((colorInt & 0x00FF00) >> 8) / 255.0,
136-
blue: CGFloat(colorInt & 0x0000FF) / 255.0,
137-
alpha: alpha
138-
)
139-
}
140-
141-
private func makeCGPoint(_ point: [CGFloat], _ scaleX: CGFloat, _ scaleY: CGFloat) -> CGPoint {
142-
return CGPoint(x: scaleX * point[0], y: scaleY * point[1])
143-
}
144-
145-
private func computeDist(_ a: [CGFloat], _ b: [CGFloat], scaleX: CGFloat, scaleY: CGFloat) -> CGFloat {
146-
return hypot(scaleX * (a[0] - b[0]), scaleY * (a[1] - b[1]))
147-
}
148-
149-
private func computePath(_ context: CGContext, _ coordinates: [[CGFloat]], scaleX: CGFloat, scaleY: CGFloat) {
150-
// Start path at the first point.
151-
var prevPoint = coordinates[0]
152-
context.move(to: makeCGPoint(prevPoint, scaleX, scaleY))
153-
for point in coordinates.dropFirst() {
154-
guard computeDist(prevPoint, point, scaleX: scaleX, scaleY: scaleY) > 3 else {
155-
// Smooth small irregularities.
156-
continue
157-
}
158-
let midX = (prevPoint[0] + point[0]) / 2
159-
let midY = (prevPoint[1] + point[1]) / 2
160-
// Draw line to the midpoint between the next two points. Use the first
161-
// point as curve control (line will bend toward it).
162-
context.addQuadCurve(
163-
to: makeCGPoint([midX, midY], scaleX, scaleY),
164-
control: makeCGPoint(prevPoint, scaleX, scaleY)
165-
)
166-
prevPoint = point
167-
}
168-
// Draw line to the last point.
169-
prevPoint = coordinates.last!
170-
context.addLine(to: makeCGPoint(prevPoint, scaleX, scaleY))
171-
}
172-
173-
private func renderAnnotation(_ context: CGContext, scaleX: CGFloat, scaleY: CGFloat) {
174-
guard page < annotationData.count else {
175-
// No annotation data for current page.
176-
return
177-
}
178-
let annotationPage = annotationData[page]
179-
180-
// Draw strokes.
181-
context.setLineCap(.round)
182-
context.setLineJoin(.round)
183-
for stroke in annotationPage.strokes {
184-
guard stroke.path.count > 1 else {
185-
continue
186-
}
187-
context.setStrokeColor(parseColor(stroke.color).cgColor)
188-
context.setLineWidth(stroke.width)
189-
190-
context.beginPath()
191-
computePath(context, stroke.path, scaleX: scaleX, scaleY: scaleY)
192-
context.strokePath()
193-
}
194-
195-
// Draw text.
196-
for msg in annotationPage.text {
197-
// Increase the font for larger views, but do so at a reduced rate.
198-
let scaledFont = 9 + (msg.fontSize * scaleX) / 1000
199-
msg.str.draw(
200-
at: makeCGPoint(msg.point, scaleX, scaleY),
201-
withAttributes: [
202-
.font: UIFont.systemFont(ofSize: scaledFont),
203-
.foregroundColor: parseColor(msg.color)
204-
]
205-
)
206-
}
207-
}
208-
209133
private func renderPdf() {
210134
guard !frame.isEmpty && !source.isEmpty && readyToRender else {
211135
// View layout not yet complete, or nothing to render.
@@ -270,10 +194,6 @@ public class PdfView: UIView {
270194
context.setRenderingIntent(.defaultIntent)
271195
context.drawPDFPage(pdfPage)
272196
context.restoreGState()
273-
274-
context.saveGState()
275-
self.renderAnnotation(context, scaleX: currentFrame.width, scaleY: currentFrame.height)
276-
context.restoreGState()
277197
}
278198

279199
DispatchQueue.main.async {

0 commit comments

Comments
 (0)