06/23 Fix inspection layout display and exported PDF file (sent via email)

This commit is contained in:
Nguyen Ngo
2026-06-23 12:12:47 -04:00
parent 47ff9f3aa5
commit 32caf735e8
3 changed files with 52 additions and 24 deletions
+33 -15
View File
@@ -29,17 +29,13 @@ private let kFtrH: CGFloat = 39.6 // footer area height
private let kTopY: CGFloat = kHdrH + 14 // first content Y on a fresh page
private let kBotY: CGFloat = kH - kFtrH // lowest Y before footer
// Image compression keeps the PDF (and thus the email attachment) small
// regardless of source photo resolution. Same values as IssuePDFGenerator.
private let kImgMaxPx: CGFloat = 700
private let kImgJPEGQuality: CGFloat = 0.55
/// Downscale to kImgMaxPx on the longer side and re-encode as JPEG at
/// kImgJPEGQuality, returning a fresh UIImage built from the compressed
/// bytes. Applied to every photo before it's embedded in the PDF this is
/// the main lever for keeping file size minimal.
/// Downscale to 700px on the longer side and re-encode as JPEG quality 0.55,
/// returning a fresh UIImage built from the compressed bytes. Applied to
/// every photo before it's embedded in the PDF keeps file size minimal.
/// nonisolated: called from inside a TaskGroup (concurrent), not @MainActor.
private nonisolated func compress(_ image: UIImage) -> UIImage? {
let kImgMaxPx: CGFloat = 700
let kImgJPEGQuality: CGFloat = 0.55
let size = image.size
guard size.width > 0, size.height > 0 else { return nil }
let scale = min(kImgMaxPx / size.width, kImgMaxPx / size.height, 1.0)
@@ -412,26 +408,48 @@ private func drawFormFields(_ pctx: PDFContext,
let baseRowH: CGFloat = 28
let photoRowH: CGFloat = 92 // taller row to fit an embedded photo
// Group by original row
// Sort fields by (row, col) before grouping mirrors the web PDF generator:
// form_fields = sorted(schema, key=lambda f: (f['row'], f['col']))
// Without this, schema fields arrive in JSON-array/editor-insertion order,
// which can place section headers after the data rows they belong to.
let sortedSchema = schema.sorted {
let r0 = $0["row"] as? Int ?? 0, r1 = $1["row"] as? Int ?? 0
if r0 != r1 { return r0 < r1 }
let c0 = $0["col"] as? Int ?? 0, c1 = $1["col"] as? Int ?? 0
return c0 < c1
}
// Group by original row, tracking which section header precedes each new row.
// Uses a queue (not a single pendingSec variable) so that when a section
// field is encountered but the next few fields are on rows already in
// rowGroups, the section label is not silently dropped or overwritten by the
// next section before it was consumed.
var rowGroups: [Int: [[String: Any]]] = [:]
var rowOrder: [Int] = []
var pendingSec: String? = nil
var sectionQueue: [String] = [] // pending section labels, in schema order
var secForRow: [Int: String] = [:]
for f in schema {
for f in sortedSchema {
let ftype = f["type"] as? String ?? ""
if skipTypes.contains(ftype) { continue }
if ftype == "section" { pendingSec = f["label"] as? String ?? ""; continue }
if ftype == "section" {
sectionQueue.append(f["label"] as? String ?? "")
continue
}
let row = f["row"] as? Int ?? 1
if rowGroups[row] == nil {
rowOrder.append(row)
rowGroups[row] = []
if let s = pendingSec { secForRow[row] = s; pendingSec = nil }
// Assign the oldest pending section label to this new row.
if !sectionQueue.isEmpty {
secForRow[row] = sectionQueue.removeFirst()
}
}
rowGroups[row]!.append(f)
}
for origRow in rowOrder.sorted() {
// Iterate in rowOrder (which is now schema-sorted insertion order = row order)
for origRow in rowOrder {
guard let fields = rowGroups[origRow] else { continue }
// Section banner