// Tools for mis-formatted label PDFs (e.g. Amazon FBA carton label + DHL // Retoure label): ONE DIN-A4 PORTRAIT page carrying TWO labels that lie // SIDEWAYS on it (content rotated 90° counter-clockwise, one label in the top // half, one in the bottom half). Printing such a page on the label printer // squeezes both labels unreadably onto one label. // // splitRotateA4LabelPdf() cuts every A4-portrait page at the horizontal // midline into two DIN-A5 pages (top half first) and bakes a 90° CLOCKWISE // rotation into the content so both labels come out upright — safe for any // renderer/printer (no reliance on the /Rotate flag). Non-A4-portrait pages // are copied through unchanged. // // analyzeLabelPdf() detects such files: page size ≈ A4 portrait, plus a // content-stream heuristic that looks for 90°-rotated placement matrices // (`0 1 -1 0 … cm` style) — the signature of labels lying sideways. The // heuristic is fail-soft: if the streams can't be decoded we still report the // size-based candidate so the (manual) split button stays available. // // pdf-lib is imported dynamically so it stays out of the main bundle. const A4_PT = { w: 595.28, h: 841.89 } const SIZE_TOLERANCE_PT = 30 export const isA4PortraitPt = (w: number, h: number): boolean => Math.abs(w - A4_PT.w) <= SIZE_TOLERANCE_PT && Math.abs(h - A4_PT.h) <= SIZE_TOLERANCE_PT export interface LabelPdfAnalysis { pageCount: number a4PortraitPages: number sidewaysPages: number /** ≥1 A4-portrait page → the split & rotate action makes sense */ splitCandidate: boolean /** ≥1 A4-portrait page whose content stream shows 90°-rotated placements */ sidewaysDetected: boolean } // Decode a page's (possibly flate-compressed, possibly array-of-streams) // content into latin1 text. Returns '' on any failure. const pageContentText = (pdfLib: any, page: any): string => { try { const { PDFArray, PDFRawStream, decodePDFRawStream } = pdfLib const contents = page.node.Contents() if (!contents) return '' const streams: any[] = [] if (contents instanceof PDFArray) { for (let i = 0; i < contents.size(); i++) { const s = page.node.context.lookup(contents.get(i)) if (s instanceof PDFRawStream) streams.push(s) } } else if (contents instanceof PDFRawStream) { streams.push(contents) } let text = '' for (const s of streams) { const bytes = decodePDFRawStream(s).decode() let chunk = '' for (let i = 0; i < bytes.length; i++) chunk += String.fromCharCode(bytes[i]) text += chunk + '\n' } return text } catch { return '' } } // True when the content stream contains at least one 90°-rotation placement // matrix (a≈0, d≈0, b/c opposite-signed) — labels drawn sideways. const hasSidewaysPlacement = (content: string): boolean => { if (!content) return false const reCm = /(-?[\d.]+)\s+(-?[\d.]+)\s+(-?[\d.]+)\s+(-?[\d.]+)\s+(-?[\d.]+)\s+(-?[\d.]+)\s+cm\b/g let m: RegExpExecArray | null while ((m = reCm.exec(content)) !== null) { const a = Number(m[1]); const b = Number(m[2]); const c = Number(m[3]); const d = Number(m[4]) if (Math.abs(a) < 0.01 && Math.abs(d) < 0.01 && Math.abs(b) > 0.5 && Math.abs(c) > 0.5 && b * c < 0) { return true } } return false } export const analyzeLabelPdf = async (input: ArrayBuffer | Uint8Array): Promise => { const pdfLib: any = await import('pdf-lib') const doc = await pdfLib.PDFDocument.load(input, { ignoreEncryption: true }) const result: LabelPdfAnalysis = { pageCount: doc.getPageCount(), a4PortraitPages: 0, sidewaysPages: 0, splitCandidate: false, sidewaysDetected: false, } for (let i = 0; i < result.pageCount; i++) { const page = doc.getPage(i) const { width, height } = page.getSize() if (page.getRotation().angle % 360 !== 0) continue if (!isA4PortraitPt(width, height)) continue result.a4PortraitPages++ if (hasSidewaysPlacement(pageContentText(pdfLib, page))) result.sidewaysPages++ } result.splitCandidate = result.a4PortraitPages > 0 result.sidewaysDetected = result.sidewaysPages > 0 return result } export const splitRotateA4LabelPdf = async (input: ArrayBuffer | Uint8Array): Promise => { const { PDFDocument, degrees } = await import('pdf-lib') const src = await PDFDocument.load(input, { ignoreEncryption: true }) const out = await PDFDocument.create() const pageCount = src.getPageCount() for (let i = 0; i < pageCount; i++) { const page = src.getPage(i) const { width: w, height: h } = page.getSize() if (page.getRotation().angle % 360 !== 0 || !isA4PortraitPt(w, h)) { const [copied] = await out.copyPages(src, [i]) out.addPage(copied) continue } const halfH = h / 2 // Top half first, then bottom half. The transformationMatrix shifts the // half down to the origin — drawing with a negative x/y offset instead // renders blank in some viewers, so keep the normalization in the matrix. for (const bottom of [halfH, 0]) { const emb = await out.embedPage( page, { left: 0, bottom, right: w, top: bottom + halfH }, [1, 0, 0, 1, 0, -bottom] ) const a5 = out.addPage([halfH, w]) // DIN A5 portrait (420.9 × 595.3 pt) // rotate -90 at (0, w): (u,v) → (v, w-u) — bakes the clockwise turn a5.drawPage(emb, { x: 0, y: w, rotate: degrees(-90) }) } } return await out.save() } // Uint8Array → base64 (chunked; label PDFs are small but stay stack-safe) export const pdfBytesToBase64 = (bytes: Uint8Array): string => { let binary = '' const chunk = 0x8000 for (let i = 0; i < bytes.length; i += chunk) { binary += String.fromCharCode.apply(null, bytes.subarray(i, i + chunk) as unknown as number[]) } return btoa(binary) }