# Dynamsoft Codepool — Article Prose Corpus > The prose of every published Dynamsoft Codepool tutorial: barcode > scanning, MRZ scanning, document capture, document viewing, OCR and Web TWAIN > integrations, with sample code on GitHub. > > This is the full-text companion to /codepool/llms.txt, which is the index of > the same articles with one-line summaries. If you only need to decide which > articles to read, fetch llms.txt — it is far smaller. If you are quoting, > summarising or extracting from Codepool content, this file has the material. > > Code blocks are omitted; inline API names and section headings are retained. > Use each article's canonical URL below for complete code, images and links. > Unpublished drafts are not present. ## Category: Barcode ### How to Read a Boarding Pass Barcode in JavaScript (IATA BCBP) URL: https://www.dynamsoft.com/codepool/read-boarding-pass-barcode-javascript.html Summary: Read the IATA BCBP barcode on a boarding pass in JavaScript with Dynamsoft Barcode Reader, then parse the Resolution 792 payload into structured fields — passenger, PNR, route, flight, seat, baggage and frequent flyer data — with nine structural checks and WebCrypto verification of the security section's ECDSA signature. Author: Xiao Ling Published: 2026-09-18 (updated 2026-09-23) Topics: Barcode, IATA BCBP, Boarding Pass, PDF417, Aztec, Data Matrix, JavaScript, Web, HTML5Reading a boarding pass barcode is two problems, not one. The first is decoding the symbol: a printed pass carries PDF417, while Aztec Code, QR Code and Data Matrix appear on mobile passes and on printed passes from version 7 — and whichever symbol it is, it is often curved, glared or photographed at an angle. The second is understanding what came out: an IATA BCBP payload is a fixed-order binary layout, not a string you can split on commas. Dynamsoft Barcode Reader handles the first problem, including the symbologies and the difficult captures. The second is a parsing job with real substance to it: the payload is positional, and its optional sections are measured by three nested size fields. This article therefore pairs the two — the Reader finds and decodes the symbol, and bcbp.js , a small standalone parser that ships with the demo, turns the resulting string into named fields and verifies the security section’s signature when the pass carries one. What you’ll build: a browser-side boarding pass scanner — camera and image input, BCBP parsing across all four boarding pass symbologies, readable labels for the code lists, the scanned image shown beside the parsed fields, nine structural checks, signature verification with a Require signature switch, and a JSON export. ## Online Demo https://www.dynamsoft.com/codepool/demos/boarding-pass-scanner/ ## Demo Video ## Key Takeaways - Two jobs, two tools. Dynamsoft Barcode Reader locates and decodes the symbol; the BCBP field layout is read by bcbp.js , a standalone Resolution 792 parser shared with the companion generator. - A boarding pass carries a 2D symbol. PDF417 and MicroPDF417 on printed passes, plus Aztec Code, QR Code, Micro QR and Data Matrix on mobile and version-7 printed passes. Restrict the format mask to those and the reader stops reporting unrelated codes. - Dynamsoft.DBR.EnumBarcodeFormat values are BigInt in 11.x. var mask = 0; mask |= v throws Cannot mix BigInt and other types ; the accumulator has to start at 0n . - The Aztec enum member is BF_AZTEC , not BF_AZTEC_CODE . A misspelled name narrows the mask silently and that symbology simply never matches. - The variable field is not self-delimiting. Each leg ends with a two-hex-digit byte count (item 6), and inside that field two more counters (items 10 and 17) size the conditional blocks — so a parser follows three nested counters rather than looking for a terminator. - A transparent PNG can fail to decode. Canvas-exported symbols carry an alpha channel, and (0,0,0,0) read as RGBA is black — which fills the quiet zone with ink. Composite onto white first. - Show the source image beside the parse. Parsed fields are hard to trust on their own; with the picture next to them, a wrong value is immediately attributable to a bad crop or a bad encode rather than to the reader. - A signature is evidence, not identity — and requiring one is the reader’s policy. The security section (items 25–30) can carry an ECDSA P-256 signature; this scanner verifies it against the generator’s demo public key (green when it matches, red when it does not), and a Require signature switch turns an unsigned pass into a refusal. BCBP itself never demands any of it: a payload that parses proves the data is well formed, not that the reservation is real. - Verification strips the signature before checking it. Items 25–30 are cut off back to the end of the last leg, and the remaining bytes are what the public key is checked against — otherwise item 30 would be part of the message it signs. ## Common Developer Questions ### How do I read a boarding pass barcode in JavaScript? Decode the 2D symbol with a barcode SDK to get the payload string, then parse that string against the IATA Resolution 792 layout. In the browser, Dynamsoft Barcode Reader decodes PDF417, Aztec Code, QR Code and Data Matrix from a camera stream or an uploaded image; the payload is then split by fixed field widths, with three nested hexadecimal size fields telling you where each optional block ends. ### How do I turn the decoded BCBP payload into named fields? Use a Resolution 792 parser. Once the Reader has returned the payload string, the fields are fixed widths inside ordered blocks, and the optional sections are delimited by three nested hexadecimal size fields rather than by separators — so the parser walks the counters and slices each item. This article uses bcbp.js , the standalone parser the demo ships with, which also resolves the code lists (compartment J to Business (premium) , status 1 to Checked in ) and runs nine structural checks on the result. ### Which barcode symbologies appear on a boarding pass? Printed passes use PDF417 , and since Resolution 792 version 7 (2018) may also use Aztec Code, QR Code or Data Matrix . Mobile passes have used the 2D symbologies since version 2. Restricting the Reader to BF_PDF417 , BF_MICRO_PDF417 , BF_AZTEC , BF_QR_CODE , BF_MICRO_QR and BF_DATAMATRIX covers every boarding pass and avoids reporting unrelated codes in the same frame. ### Why does my code throw “Cannot mix BigInt and other types”? Dynamsoft.DBR.EnumBarcodeFormat values are BigInt in 11.x, because the format mask spans more than 32 bits and a JavaScript number cannot hold it. Build the mask with a BigInt accumulator: ### What does the scanner check besides decoding the barcode? Nine structural checks on the payload: the format code is M , the computed length equals the actual length, the leg count matches the data, a version marker is present and recognised, each airport code is three letters, each Julian date is within 001 – 366 , each seat follows the row-plus-letter form, each leg’s declared block size agrees with the data present, and — when a security section is present — the byte count item 29 declares matches the item 30 data actually in the payload. These catch a payload that decoded correctly but was not written correctly. The signature verdict reported in Step 8 is a separate question: it answers whether the bytes were edited , not whether they are well formed. ### How do you verify a boarding pass signature in JavaScript? Import the issuer’s public key with WebCrypto ( crypto.subtle.importKey('spki', ...) for an ECDSA P-256 key), cut items 25–30 off the payload back to the end of the last leg, decode item 30 from base64 into its raw 64 bytes, and call crypto.subtle.verify() over those stripped bytes — the same trimming on both sides is what makes the comparison meaningful. In this demo the whole thing is Bcbp.verifySecurityData(payload, DEMO_PUBLIC_KEY) , which resolves to one of five states: ok (signature matches), bad (edited, truncated or signed with another key), none (no security section — nothing to verify, not an error), unsupported (WebCrypto unavailable) or error (the payload did not decode at all). ## Prerequisites - A Dynamsoft Barcode Reader license key. The 24-hour trial key works for a first run; a 30-day free trial license covers development and testing. - A browser with camera access for live scanning, or any browser for image upload. - A local web server. The page loads sibling folders, so file:// will not do. - Some boarding pass images. The demo ships seven with known payloads, produced by the boarding pass generator . ## Step 1: Load the SDK and restrict the symbologies Activate the license before creating any component, then load only the module you need: The format mask is where both traps live — the BigInt accumulator and the enum member name — so it is worth writing defensively: The missing list is worth keeping. BF_AZTEC_CODE looks plausible but is not the real member name, and a name that does not exist on the enum is undefined — which ORs to nothing and excludes that symbology without raising anything. With the check in place the console says exactly what is wrong: Applying the mask goes through the simplified settings of the preset template: Note the try / catch . Narrowing the symbologies makes the reader faster and cuts spurious detections, but it is not required for correctness, so a failure to build the mask should degrade to the preset default instead of taking the whole page down. ## Step 2: Decode from an image, and composite onto white Camera frames flow through the router continuously. An uploaded image is different: it is decoded on demand with capture() , which takes an image object rather than a file. The shape matters — it must be the same one the SDK builds internally when its own image view hands over a picked file: Before getImageData , fill the canvas white: That last comment is not hypothetical. A symbol exported from an HTML canvas has a transparent background unless you filled it. The demo’s own sample images originally failed to decode for exactly this reason: the file looked right in an image viewer, and the reader saw a solid black rectangle. Every “transparent” pixel is (0,0,0,0) , and the alpha channel is ignored on the path into the SDK. The scanner takes either input. Upload mode accepts a file, a drop or a clipboard paste, and ships the seven generated samples so you can try it without a boarding pass to hand: ### Showing the image next to the parse Parsed fields on their own are hard to trust. When the panel also shows the picture the barcode came from, a wrong field is immediately attributable — a blurry crop, the wrong half of the pass, or a genuinely misencoded symbol. The image path already has the source, so passing it through is one extra argument; the camera path can produce one too by snapshotting the viewfinder: The image is bounded by its own aspect ratio rather than stretched, so a tall paper pass and a wide bare symbol both stay readable, and clicking it opens the full-size view: ## Step 3: Split the work between the Reader and the parser It is worth being precise about who does what, because the division shapes the whole demo: Step | Who does it | Locate and decode the symbol, whatever the capture conditions | Dynamsoft Barcode Reader | Split the string into items, follow the nested size fields, decode the code lists | bcbp.js | The Reader’s job is the hard imaging one, and it is the part a generic decoder struggles with: finding and reading a 2D symbol that is small, curved, glared or photographed at an angle. The parsing job is a different kind of problem — positional, with nested length counters — and it belongs in a parser that you can read, test against known payloads and reuse. bcbp.js is that parser: one dependency-free file that the generator encodes with and the scanner decodes with, so the two cannot drift apart. That also keeps the parser testable in isolation. It is checked against the official Resolution 792 example payloads, which is a test you can run without a camera or a browser. ## Step 4: Read the mandatory section The fixed part is 58 characters plus a 2-character size field. Because the widths are fixed, reading it is a sequence of slices: Then each leg reads its 35 characters in order. Most fields only need trimming, but the seat, the flight number and the check-in sequence are stored padded for the scanner’s benefit, not for display: 018A reads back as 18A , 0025 as 25 , 0834 as 834 . Displaying the padded form is a common way to make a scanner look wrong when it is in fact right. ## Step 5: Follow the nested size fields This is the core of the parser. A BCBP variable field is not terminated by a delimiter; it is measured by the field in front of it, three counters deep: Three details carry most of the weight here: - The unique block appears only on the first leg. It is preceded by > and the version digit, so the check for body.charAt(0) === '>' is also what tells you which layout you are reading. - A later leg starts straight at the item 17 size field. It has no version marker and no unique block, so its variable field is item 17 hex + repeated data + item 4 . - Whatever is left after the repeated block is item 4 , the carrier’s own data. It is free-form and should be preserved rather than discarded — the round trip depends on it. A pre-2008 payload has item 6 = 00 and no variable field at all — not even the item 17 size field. Treating that as “a zero-length block” is right; treating it as “then read the item 17 size field” is not. Keeping the raw content visible makes the result auditable rather than merely plausible: ## Step 6: Decode the conditional items Once you have the two blocks as substrings, each item is a fixed-width slice, and the code lists turn the single-character values into something readable: Two of these deserve a second look, because both are counter-intuitive: The baggage tag is structured, not an opaque number. Thirteen digits: a 10-digit licence plate followed by a 3-digit count of consecutive bags. The plate itself starts with a leading zero, then the carrier’s three-digit numeric code: So 0014123456002 is Air Canada (numeric code 014 ) with 2 bags — which is more useful to display than the raw digits. The date of issue stores only the last digit of the year. Item 22 is four characters: one digit of year plus a Julian day. 6261 means “year ending in 6, day 261” — which could be 2006, 2016 or 2026. Resolving it against the reference year used for the flight date is the only reading that works: Hard-coding 2000 + digit is the obvious shortcut, and it is wrong: it reports a pass issued in 2026 as 2006. Resolving against the reference year fixes that — and it also makes the official example correct. Its issue date is 1325 , a year ending in 1: Reference year | Resolved issue date | 2026 (today) | 2021-11-21 | 2012 | 2011-11-21, the date the example was actually issued | ## Step 7: Report structural checks, not just fields Fields alone do not tell you whether a payload is well formed. Nine checks do, and each one comes with its evidence so a disagreement is actionable: The length check is computed from the payload’s own declarations rather than from the actual length, which is what makes it a check at all: This matters for real-world data, because not every boarding pass in the wild is conformant. A payload can declare four legs and carry three, declare a 103-byte block and provide 90, or omit the version marker while still carrying conditional data. The checks surface those instead of letting the UI present a half-parsed pass as if it were clean. ## Step 8: Verify the security section’s signature (items 25–30) Parsing and checking length say the payload is well formed; they say nothing about whether the bytes were edited after issue. A payload can end with a security section — ^ (item 25), one character of security data type (item 28), two hexadecimal characters of byte length (item 29), then the data itself — and the companion generator writes one automatically with ECDSA P-256. A reader needs only the public half of the key pair. verifySecurityData() imports that key once (SPKI format), strips items 25–30 with the same securityBase() helper the generator signed with, and hands the remaining bytes to WebCrypto. A value that is not even base64 fails before any key is involved: Verification is asynchronous, so the signature box renders first with Verifying… and fills in when the promise resolves. The five states map onto three colours: - ok — green: the signature matches the bytes that remain after stripping. - bad — red: present but does not verify — the payload was edited after signing, signed with a different key, truncated so item 29’s declared length disagrees with the data, or not base64. - none — neutral: items 25–30 are absent. An unsigned pass is legal in BCBP, so nothing is wrong with it — unless this reader has been told otherwise. - unsupported / error — amber: WebCrypto is unavailable, or the payload did not decode, so no verdict is possible at all. Whether none is acceptable is policy, not format, so it lives in a switch rather than in the parser. Turning on Require signature re-reads whatever is already on screen — no second scan needed — and the none branch flips from an informative note to a refusal: The verdict also lands in the JSON export as signature.state and signature.accepted , so a test harness can assert on it: an unsigned pass counts as accepted only while Require signature is off. ## Common Issues and Edge Cases Aztec symbols are never detected. The enum member is BF_AZTEC . Using BF_AZTEC_CODE produces undefined , and mask |= undefined contributes nothing, so Aztec is silently excluded. Log any name the enum does not define. Cannot mix BigInt and other types when building the mask. The format enum is BigInt in 11.x. Start the accumulator at 0n . A transparent PNG of a symbol does not decode. Canvas-exported symbols have an alpha channel and a transparent pixel is (0,0,0,0) ; read as RGBA that is black, so the quiet zone fills with ink. Composite the image onto white before getImageData . The results panel re-opens dozens of times a second in camera mode. Consecutive frames see the same symbol. Add a MultiFrameResultCrossFilter with enableResultDeduplication('barcode', true) , and guard the receiver with an “already showing results” flag. Passing a Blob or a canvas to capture() returns no results. The router expects a byte buffer in the getImageData() shape — RGBA, stride = 4 × width , format: 10 . Building that object explicitly is the difference between zero barcodes and a correct decode. stopCapturing() may return undefined . It is safe to await , but not safe to chain .then() onto. Wrap it: Promise.resolve(cvRouter.stopCapturing()).then(...) . Sample chips inside the drop zone open the file picker as well as scanning. The drop zone’s own click handler fires too. Call event.stopPropagation() in the chip handler. The flight date can be out by a year. There is no year in the payload. Item 46 is a Julian day of the year, so a pass read in January for a December flight, or read five years later, can only be resolved by inference — and it will be wrong at the boundary. Report the Julian day alongside the resolved date so the ambiguity is visible. A signature that verified a moment ago turns red after editing one field. The signature covers the payload up to the end of the last leg, so changing the name, flight or seat changes those bytes and the old signature stops matching — that is the feature working, not a bug. Regenerate the pass with Re-sign in the companion generator rather than expecting a signature to survive an edit. “Item 29 declares 88 bytes but only 78 are present.” Item 29 counts the bytes of item 30, so a truncated signature fails its own length check before any key is involved. The fix is on the writer’s side: re-sign so item 29 and item 30 agree again. ## Conclusion The split matters. A barcode SDK solves the imaging problem: finding and decoding a 2D symbol on a curved, glared or poorly-lit boarding pass. The BCBP layout is a separate parsing problem, with three nested size fields and a positional item order. Keeping the two apart makes both testable, and writing the parser against the official Resolution 792 examples is what keeps it honest. Signature verification rides along in the parser rather than the SDK: the reader’s job ends at the string, and WebCrypto answers the separate question of whether the bytes were edited after issue. The companion article builds the other half of the loop: how to generate a boarding pass barcode in JavaScript , which is also where the sample images in this demo come from. ## Source Code Get the complete sample project source code on GitHub ### How to Generate a Boarding Pass Barcode in JavaScript (IATA BCBP) URL: https://www.dynamsoft.com/codepool/generate-boarding-pass-barcode-javascript.html Summary: Build a browser-side IATA BCBP boarding pass generator in JavaScript: write a Resolution 792 payload with its nested size fields, auto-sign the security section (items 25–30) with WebCrypto ECDSA P-256, render it as PDF417, Aztec Code, QR Code or Data Matrix, draw a printed or mobile pass preview on an HTML5 canvas, and print the exact field values a conformant scanner should return. Author: Xiao Ling Published: 2026-09-18 (updated 2026-09-23) Topics: Barcode, IATA BCBP, Boarding Pass, PDF417, Aztec, QR Code, JavaScript, Web, HTML5The barcode on a boarding pass carries a payload defined by IATA Resolution 792 — an IATA Bar Coded Boarding Pass (BCBP). It is a fixed-order layout with mandatory and conditional sections, strictly sized fields, and two nested hexadecimal size fields that a reader has to follow to find where anything ends. That structure is what makes a boarding pass worth generating rather than typing text into a generic QR generator. A payload is only useful as a test case if it is well formed — correct widths, correct padding, consistent size fields — and if you know what the scanner should return, so that a wrong or missing field stands out. This article builds that generator: it composes a BCBP payload, signs the security section (items 25–30) with WebCrypto so an edited field can be caught later, renders the result as PDF417, Aztec Code, QR Code or Data Matrix on a printed or mobile pass preview, and prints the field-by-field result a conformant scanner should return. What you’ll build: a browser-side BCBP generator — the Resolution 792 encoder, the nested size fields, one-to-four flight legs, four symbologies, a security section that re-signs itself whenever a field changes, a canvas pass preview that marks what Randomise changed, and a self-check that parses the page’s own output. ## Online demo https://www.dynamsoft.com/codepool/demos/boarding-pass-generator/ ## Demo Video ## Key Takeaways - BCBP is a fixed layout, not a key-value format. There are no field tags. Items are written in a fixed order inside a block, and only trailing optional items may be omitted. - item 6 is the byte count of everything that follows it in that leg’s variable field. It is the two hex digits after each leg’s fixed block, and it counts the two nested size fields as well as the data. Computing it from the assembled bytes is the one definition that cannot drift out of step. - Padding is part of the format. The check-in sequence is four digits plus a trailing blank ( 0025 ), and the seat is a three-digit zero-padded row plus a letter ( 018A ). A scanner expects that exact spelling. - PDF417 is the printed-pass default; Aztec, QR Code and Data Matrix only became standard for printed passes with version 7 (2018). Mobile passes have used the 2D symbologies since version 2. - The flight date has no year in it. BCBP stores a Julian day of the year ( 001 – 366 ), so the year has to be inferred from the issue date or the current date. - BCBP never verifies a signature for you — but a pass can carry one. The security section (items 25–30) holds a signature over the payload; this generator writes it automatically with WebCrypto (ECDSA P-256), and checking it is the reader’s job. A signature proves the bytes were not edited, never that a reservation exists. - Sign first, append second. Item 30 lives inside the payload it would sign, so the signature covers everything up to the end of the last leg and items 25–30 are appended afterwards — the verifier strips that same section off before checking, so both sides always compare the same bytes. - Mark what a bulk action changed. A button that rewrites a dozen fields at once needs to show which ones moved, or a value you set deliberately is indistinguishable from one that was overwritten. ## Common Developer Questions ### How do I generate a boarding pass barcode in JavaScript? Compose the IATA BCBP payload as a string, then hand that string to a barcode encoder. The payload is plain ASCII: a 23-character unique mandatory section, a 35-character block that repeats per leg, a two-character hexadecimal size field, and the leg’s variable field with two more nested size fields inside it. In the browser, bwip-js renders the string as PDF417, Aztec Code, QR Code or Data Matrix on a canvas, which you can export as a PNG. ### What is the difference between IATA BCBP and a QR code? QR Code is one of the symbologies a BCBP payload can be carried in; BCBP is the data format. Since Resolution 792 version 7, a printed pass may use Aztec Code, QR Code or Data Matrix as well as the original PDF417, and a mobile pass has used the 2D symbologies since version 2. The same payload string can be encoded in any of them, so the format and the symbology are independent choices. ### Which barcode format is used on a boarding pass? PDF417 is the format Resolution 792 originally specified and still the default on paper passes. From version 7 (2018), printed passes may also use Aztec Code, QR Code or Data Matrix . Mobile boarding passes typically carry the same payload in one of those 2D symbologies because they render more compactly on a phone screen. ### How long is a BCBP payload? The fixed part of a single-leg pass is 60 characters : 23 characters of unique mandatory section, 35 characters of per-leg mandatory data, and a 2-character hexadecimal size field. The variable field that follows adds between 0 and 255 bytes, so a complete single-leg payload is 60 characters plus whatever conditional data you include. Each further leg adds another 35-character block and its own size field. ### Why does the flight date in my payload not have a year? The standard stores a day of the year, not a date. Item 46 is a three-digit Julian day, so 261 means the 261st day — 18 September, with no year attached. A reader has to infer the year from the date of issue (item 22, which stores the last digit of the year) or from the current date. This is why a boarding pass issued at the turn of the year can be genuinely ambiguous. ### Does the generated boarding pass include a digital signature? Yes. Item 30 holds an 88-character base64 ECDSA P-256 signature over the payload — everything up to the end of the last leg — and the page re-signs it every time any field changes, using WebCrypto entirely in the browser. Editing the signature field by hand latches the value so you can build a deliberate failure case, clearing it omits items 25–30 entirely, and the Re-sign button puts a valid signature back. The companion scanner article verifies those same bytes against the public half of the key pair. ### Can I use a generated boarding pass barcode to travel? No. The pass is signed with a demo key that ships in the page source, and every other field is whatever the form was filled with — the payload carries no proof of a booking behind it. Real carriers sign on their issuing server with a key that never leaves it, verified against a public key the reader already trusts. A synthetic pass exists to test a scanner, not to board a flight. ## Prerequisites - Node.js or any static file server for local development. The page itself has no build step. - A browser with an HTML5 canvas — the symbol and the preview are both drawn client-side. - bwip-js (BWIPP compiled to JavaScript), loaded from a CDN. It is the only third-party dependency. - To read the symbols back with Dynamsoft Barcode Reader, you need a license key. A 30-day free trial license covers development and testing. ## Step 1: Lay out the BCBP field table Start by writing down the layout, because every later step depends on these widths. The unique mandatory section is 23 characters: Pos | Len | Item | Field | 1 | 1 | 1 | Format code, always M | 2 | 1 | 5 | Number of legs encoded (1–4) | 3 | 20 | 11 | Passenger name, SURNAME/GIVEN NAME , left-justified | 23 | 1 | 253 | Electronic ticket indicator, E or blank | Then a 35-character block that repeats for every leg : Pos | Len | Item | Field | 24 | 7 | 7 | Operating carrier PNR | 31 | 3 | 26 | From airport (IATA) | 34 | 3 | 38 | To airport (IATA) | 37 | 3 | 42 | Operating carrier designator | 40 | 5 | 43 | Flight number, 4 digits + optional letter | 45 | 3 | 46 | Date of flight, Julian day of year | 48 | 1 | 71 | Compartment code | 49 | 4 | 104 | Seat number, 3-digit row + letter | 53 | 5 | 107 | Check-in sequence number | 58 | 1 | 113 | Passenger status | Positions 59–60 are item 6, the size field. Encode the fixed part first, with the padding rules spelled out: The padding rules are the part that catches people out. Alphanumeric items are left-justified with trailing blanks; numeric items carry leading zeros. The seat and the sequence number look similar but pad differently: Seat 18A becomes 018A — leading zeros. Sequence 42 becomes 0042 — leading zeros and a trailing blank. The official Resolution 792 examples spell it that way ( 0025 , 0027 ), and a scanner has to cope with that exact spelling, so the generator reproduces it rather than choosing the tidier-looking right-aligned form. ## Step 2: Pack the conditional sections in fixed order The variable field holds three things, one after another: the once-per-pass data, the once-per-leg data, and the carrier’s own space. Both conditional blocks are a fixed sequence of slots with known widths, which is what lets a reader parse them without tags: The complete once-per-pass block (item 10): Item | Len | Field | 15 | 1 | Passenger description | 12 | 1 | Source of check-in | 14 | 1 | Source of issuance | 22 | 4 | Date of issue — last digit of the year plus the Julian day | 16 | 1 | Document type | 21 | 3 | Airline designator of the issuer | 23 | 13 | Baggage tag; up to three, so up to 39 characters | 31 | 13 | Non-consecutive baggage tag | 32 | 13 | Second non-consecutive baggage tag | And the once-per-leg block (item 17): Item | Len | Field | 142 | 3 | Airline numeric code | 143 | 10 | Document form / serial number | 18 | 1 | Selectee indicator | 108 | 1 | International documentation verification | 19 | 3 | Marketing carrier designator | 20 | 3 | Frequent flyer airline | 236 | 16 | Frequent flyer number | 89 | 1 | ID / AD indicator | 118 | 3 | Free baggage allowance | 254 | 1 | Fast track | Packing then means concatenating slots until the last non-empty one, and dropping only the trailing empties: Because the block is positional, a field can only be present if every field before it is present too. One consequence is worth surfacing in the UI: if a user fills in a later item and leaves an earlier one blank, that blank still gets packed, with its own padding character — data the user never entered. Writing a frequent flyer number therefore forces the free baggage allowance in front of it to appear as blanks. The validator looks for that gap and reports it instead of silently inventing the filler: One block remains at the very end of the variable field: the security section, items 25–30. It is not typed into the form — it is signed — and Step 8 covers it. ## Step 3: Write the size fields This is the step that breaks generated passes. The variable field is not self-delimiting; it is measured by the field in front of it, and it contains two smaller size fields of its own. Writing it is easiest if you assemble the bytes first and then take their length: Reading the assembled block back makes the arithmetic self-evident: - > plus the version digit — 2 bytes (items 8 and 9) - the item 10 size field — 2 bytes - the unique conditional data - the item 17 size field — 2 bytes - the repeated conditional data - the airline’s own data (item 4), which is whatever remains For the official single-leg example with no conditional data, that gives item 6 = 06 : A later leg has no unique block, so its size field is just the repeated size plus the carrier data: The page shows the assembled payload so you can count the bytes yourself: ## Step 4: Support more than one leg The mandatory block repeats per leg, but the 23-character header does not — the passenger name is written once. The PNR, however, sits inside the repeating block, so a two-leg pass can carry a different locator on each leg. The official Resolution 792 two-leg example does exactly that: So the record keeps the PNR per leg, and the form propagates an edit to every leg that is still carrying the previous value: One thing to watch: the free baggage allowance (item 118) is also per leg, so a second leg with no allowance omits it — while the PNR, in the same block, still appears. A two-leg pass is a good test of your size fields, because there are three of them to get wrong instead of one. ## Step 5: Render the symbol with bwip-js The payload is a plain string, so rendering is the easy part. Map each symbology to its BWIPP encoder name and a module size: minVersion is not cosmetic. Aztec Code, QR Code and Data Matrix were only added to the standard for printed passes in version 7 , so picking one of them silently sets the version field to 7. A payload that claims version 6 while travelling in a QR Code claims a combination the standard did not allow. Also worth knowing: a symbol exported from a canvas is transparent by default. toCanvas leaves the background unset, so a PNG saved straight from it carries an alpha channel. Handed those pixels as RGBA, a reader treats (0,0,0,0) as black, which floods the quiet zone with ink and hides the symbol. Fill the canvas white first, or pass backgroundcolor: 'FFFFFF' to the encoder. ## Step 6: Draw a pass preview A preview that looks like a boarding pass is easier to sanity-check than a bare symbol: you can see at a glance that the name, route and seat are the ones you typed. The drawing is ordinary Canvas 2D; the parts worth calling out are the watermark and the symbol panel: The symbol is drawn into a tinted panel and scaled to fit, which keeps it away from the pass border — the same quiet-zone reasoning as paddingwidth above: The same payload also renders as a mobile wallet pass, which is the layout a phone would show: ### Marking what Randomise changed A button that rewrites a dozen controls at once is disorienting: nothing tells you which values moved, so a field you had set deliberately looks the same as one that was overwritten. Snapshotting the form before the rewrite and comparing after fixes that in a few lines: Two details are worth keeping: - The reflow. Removing and re-adding the class in the same frame does not restart a CSS animation — the browser coalesces the two mutations. Reading offsetWidth between them forces a style recalculation, which is the cheapest way to make the animation replay on every click. - The reduced-motion fallback. Skipping the animation entirely would remove the information, not just the motion, so the media query keeps a static colour instead: The count of changed controls is worth reporting too — it turns “did that do anything?” into a number: ## Step 7: Prove the payload by parsing it back Testing a scanner needs a generator you can verify. The most direct check is to parse your own output with the same code the scanner will use: write the decoder alongside the encoder, and treat the parse result as the test oracle rather than restating the form. Because the two directions share one field table, the table above is read back from the bytes. That also makes a round-trip test possible: parse a payload, rebuild the record, re-encode, and require the bytes to come back identical. Running that against the official Resolution 792 examples is what turns “it looks right” into a check. On page load the demo logs: The four vectors cover the awkward cases rather than the happy path: a single leg with no conditional data (where item 6 is 00 and nothing follows it — not even the item 17 size field), a single leg with unique, repeated and security data, a two-leg pass with a different PNR per leg, and a version 6 pass whose size fields are present but zero. The structural checks report the same thing to the user: ## Step 8: Sign the security section automatically The final block of the variable field is the security section — items 25–30, written once after the last leg. ^ marks the beginning (item 25), one character carries the type of security data (item 28), two hexadecimal characters carry the byte length of item 30 (item 29), and everything after that is the data itself: Signing it raises one structural problem: item 30 is inside the payload it would sign , and a signature cannot cover itself. The fix is to sign everything up to the end of the last leg, then append items 25–30 afterwards — and for the verifier to strip that same section off before checking, so both sides always compare the same bytes. One helper does the trimming for both directions: The second problem is encoding. ECDSA P-256 returns a raw 64-byte r‖s signature; hex-encoded that is 128 characters, and validate() puts a 100-character ceiling on item 30 — a hex signature is rejected before any key is involved. Base64 of the same bytes is 88 characters, comfortably inside the limit, so that is what item 30 carries. The signing itself is WebCrypto: generate() then signs on every pass: encode the record with security: null , sign those bytes with the demo private key, and write the result back into the two security fields: The demo private key ships with the page, which is the one thing a production issuer must never do: that key belongs on the issuing server, only the signature travels, and readers get the public half — exactly what the companion scanner verifies against. WebCrypto also needs a secure context, so the page must be served from http://localhost or https ; when it is not, the pass is left unsigned as typed and the hint under the field says so. Because signing runs on every change, a keystroke in either security field would be overwritten immediately — so that keystroke latches manual mode instead: the value stays exactly as typed and nothing re-signs until Re-sign or Randomise is pressed. After each render the page verifies its own output against the public key and reports the verdict in the hint: green for a signature that matches, red for one that does not (calling out the hand edit), plain when items 25–30 are absent. Re-signing without changing anything still produces a different value — ECDSA draws a fresh random nonce for every signature — but verification always strips back to the same base, so every one of those signatures checks out. ## Common Issues and Edge Cases The payload is two bytes shorter than the scanner expects. item 6 must count the two nested size fields as well as the data. The trap is computing it as “unique length + repeated length” and forgetting that '>' + version and the two 2-character size fields are inside the block too. Assemble the bytes first and take their length. item 6 = 00 but data follows. A pre-2008 pass has no version marker and no variable field at all. If you write 00 and then append conditional data, you have produced a contradictory payload. Either write a version or write no conditional data. The check-in sequence does not round trip. 42 encodes as 0042 , and reading it back gives 42 again — but if you pad it as ` 042` a scanner that compares raw strings will disagree with you. Match the examples: leading zeros, trailing blank. A name without a slash. The passenger name is SURNAME/GIVEN NAME ; item 11 is 20 characters wide and left-justified. A name with no slash is not a BCBP name, so the generator rejects it rather than guessing where the split goes. A transparent PNG of the symbol will not scan. Covered in Step 5, but it is a common “the generator is broken” report that turns out to be the export. Fill the background white. More than four legs. The standard allows at most four, even though the leg count field could hold more. Optional fields out of order. Because the blocks are positional, “item 9 present, item 4 absent” is not expressible. The validator says so rather than silently emitting filler. Item 30 is refused as too long. validate() caps item 30 at 100 characters. A P-256 signature hex-encoded is 128 and is rejected before anything is verified; base64 of the same 64 raw bytes is 88 characters, which is why that is what the generator writes. Change the signature algorithm and this ceiling is the first thing to re-check. The signature changes even when nothing else did. That is ECDSA being randomized: every signature draws a fresh nonce, so signing identical bytes again produces a different — equally valid — value. Verification strips items 25–30 first, so any signature over that same base still checks out; only an edit to the payload before ^ invalidates one. ## Conclusion Building a BCBP generator is mostly a formatting exercise, with one piece of real arithmetic: the nested size fields. If the widths, the padding and all three size fields are right, a scanner reads back every field; if any of them is wrong, it returns a partial or misread set, or a length check reports a mismatch. Writing the decoder alongside the encoder is what makes the result checkable, and running that decoder against the official Resolution 792 examples — re-encoding them and comparing bytes — is what confirms it. The security section then adds the one piece of cryptography on top: sign what you wrote, and any field edited afterwards fails to verify. The companion article covers the other half of the loop: how to read a boarding pass barcode in JavaScript with Dynamsoft Barcode Reader and the same parser. ## Source Code Get the complete sample project source code on GitHub ### How to Generate GS1 Barcodes in JavaScript for Scanner Testing URL: https://www.dynamsoft.com/codepool/generate-gs1-barcode-javascript.html Summary: Build a browser-side GS1 barcode generator in JavaScript: compose a validated GS1 element string from application identifiers, calculate GTIN and SSCC check digits, pick the symbology a real supply chain would use, render a test label on an HTML5 canvas, and export the result together with the exact parse a conformant scanner should return. Author: Xiao Ling Published: 2026-09-16 (updated 2026-09-17) Topics: Barcode, GS1, GS1 AI, DataBar, JavaScript, Web, HTML5, Data MatrixYou have written a GS1 parser, and now you need something to test it with. A real product label is the wrong answer twice over: you probably do not have the stock, and somebody else’s batch and serial numbers are not yours to put in a test fixture anyway. So you go looking for a generator, and find plenty of them — except they encode text . Type a GTIN, get a DataBar. Type (01)09506000135000(17)270430(10)LOT-42 , the way GS1 actually writes it, and they either print those characters literally into the symbol or refuse outright. A reader can decode that image perfectly and still return nothing useful, because the image was never a GS1 element string to begin with. A GS1 generator has to work the other way round. The list of application identifiers is the source of truth ; the element string, the FNC1 positions, the check digits and the symbology choice are all derived from it. Once that is the design, a second thing becomes possible: the generator can print the exact parse a conformant scanner should return, so a failing test tells you which side is wrong — the symbol or the parser. What you’ll build: a browser-side GS1 generator that composes a validated element string from application identifiers, calculates GTIN/SSCC check digits, recommends the symbology a real supply chain would use, renders a test label on an HTML5 canvas, and exports the PNG together with the table of values a scanner should report. Online demo ## Key Takeaways - Encoding for GS1 is a payload problem, not a symbology problem: get the element string right and the encoder handles the FNC1 placement for you. - The AI table does three jobs — validation, sample generation and the length rules — so it belongs in one place. - Check digits are calculated, never typed . A GTIN whose check digit is wrong is a fine test of the failure path and a useless test of everything else. - Not every symbology can carry every payload: DataBar, ITF-14 and EAN-13 encode the GTIN and nothing else , and GS1 constrains which AIs may travel together. - A long payload must be re-encoded at a smaller module size, never scaled down . A blurred linear symbol tests your camera, not your parser. - Generating the expected parse alongside the image turns each PNG into a test case instead of a picture. ## Common Developer Questions ### How do I generate a GS1 barcode in JavaScript? Compose the element string by concatenating each AI with its data, then hand that string to an encoder that understands GS1 syntax. The bwip-js library (a JavaScript port of BWIPP) accepts the human-readable (01)…(10)… form for its GS1 symbologies — gs1datamatrix , gs1qrcode , gs1_128 , databarexpanded , databaromni — and inserts the FNC1 bytes itself. ### Which barcode symbology should a GS1 payload use? It is decided by the payload, not by preference. A GTIN alone belongs in GS1 DataBar Omnidirectional (or EAN-13 for a GTIN-13). A GTIN plus batch, date or serial needs a symbology that can carry more than the GTIN: DataBar Expanded, GS1 DataMatrix, GS1 QR Code or GS1-128. An SSCC has no GTIN at all, so GS1-128 is the usual carrier. ### How do I calculate a GTIN check digit in JavaScript? Weight the digits alternately by 3 and 1 starting from the rightmost data digit, sum them, and take (10 - sum % 10) % 10 . The same mod-10 routine covers GTIN-8/12/13/14, SSCC, GLN and GSRN. ## Step 1: Make the AI Table the Model Everything else is derived from this table, so it is worth getting right first. Each entry needs four things: the code, a name, a length rule, and a way to produce a valid sample. The fixed / max distinction is not metadata — it is what tells the encoder whether a separator is needed after this element, and it is what lets a parser recover a payload whose separators were stripped. The sample() function earns its place too. Every AI in the table can produce a value that passes its own validation, so “Randomize data” is always available and a half-built payload is never blocked on typing a plausible SSCC by hand. One detail that matters more than it sounds: when Randomize data replaces the values, the fields that changed are marked for a moment . Random sample values are all digit soup — 00950600403582 and 00950600786614 look equally arbitrary — so without the marker a click reads as “nothing happened”, which is exactly the bug report this page got. The marker is a CSS animation on the row that fades out on its own and is removed from the DOM after 1.4 s, and it respects prefers-reduced-motion by dropping the animation while keeping the colour change, because the colour is the information. ## Step 2: Calculate Check Digits, Don’t Ask for Them One routine covers GTIN, SSCC, GLN and GSRN, because they all use the same mod-10 scheme. Completing the digit automatically is what makes a generated image a fair test: a scanner that verifies check digits would correctly reject a hand-typed GTIN, and you would spend the afternoon debugging the scanner. ## Step 3: Let the Payload Pick the Symbology Twelve symbologies can carry GS1 data, and they are not interchangeable: Symbology | Kind | What it can carry | GS1 DataBar Omnidirectional | 1D | GTIN, full-height retail symbol | GS1 DataBar Truncated | 1D | GTIN, shorter bars for small packaging | GS1 DataBar Stacked / Stacked Omnidirectional | 1D | GTIN, split across two rows | GS1 DataBar Limited | 1D | GTIN starting with 0 or 1, smallest DataBar | GS1 DataBar Expanded / Expanded Stacked | 1D | GTIN plus up to 74 more characters | GS1 DataMatrix | 2D | Any element string | GS1 QR Code | 2D | Any element string, consumer readable | GS1-128 | 1D | Any element string, logistics default | ITF-14 | 1D | GTIN-14 only | EAN-13 | 1D | GTIN-13 only | The recommendation is a small decision function over the payload’s AIs, and it is worth having because it encodes the field’s own habits: Validation is then relative to the chosen symbol, which is the only way it can be meaningful: a GTIN plus a batch is perfectly encodable in DataBar Expanded and completely unencodable in DataBar Omnidirectional. And every message that diagnoses a problem carries the change that resolves it. “A DataBar, ITF-14 or EAN-13 symbol carries the GTIN and nothing else” is followed by a Switch to GS1 DataMatrix button; a wrong check digit offers the corrected digit; a serial number with no trade item offers Add AI 01 ; a price in a currency with no quantity offers Add AI 30 . That is not politeness, it is the difference between a working tool and a puzzle. The GS1 length rules, the mod-10 check digit and the AI pairing constraints are specialist knowledge, and the people most likely to hit these errors are exactly the ones who do not have it yet. A message that only diagnoses leaves them to guess; a message with the remedy beside it teaches the rule and unblocks the payload at the same time. The payload model carries the remedies as data, so the UI never has to guess: The pairing rules are worth stating outright, because the encoder’s own message (“One of more requisite AIs for AI (21) are missing: 01 OR 03 OR 8006”) arrives too late to be actionable: AI | needs | why | 21 serial number | 01 , 03 or 8006 | a serial identifies a unit, so it needs the item it belongs to | 393x price in a currency | 30 , or a 31nn / 32nn / 35nn / 36nn measure | a unit price needs the quantity it is a price of | ## Step 4: Feed the Encoder the AI Syntax bwip-js is the encoder, and its GS1 symbologies accept the bracketed notation directly — the same string GS1 prints under a symbol as the human readable interpretation: BWIPP works out where the FNC1 bytes belong from the AI table: after 10 , which has no fixed length, and not after 17 , which does. That is a great deal of correctness to get for free, and it is the reason the payload model submits to the encoder as an AI list rather than as a hand-built byte string. Two symbologies do not take AI syntax, and getting them wrong produces a silently different product: That ITF-14 comment is a bug I shipped and then found. Passing 13 digits to an ITF-14 encoder looks reasonable and is wrong: the encoder appends a check digit, so a GTIN that already ended in one gets a second one computed over it. The generated label decoded cleanly to the wrong number, which is the worst kind of test fixture — it fails a scanner that is working perfectly. ## Step 5: Render a Label a Camera Can Actually Read A symbol floating on a white rectangle is not what a camera sees. Draw the thing that gets scanned: brand, product name, the identifiers in human-readable form, the symbol, and the HRI beneath it. The layout detail that matters is the symbol band: it spans the full label width, because a logistic GS1-128 with five data elements is a lot of bars and squeezing it into a narrow column is how a test label ends up undecodable. The size detail matters more. When a symbol is too wide for its frame, re-encode it at a smaller whole-number module size; do not scale the finished image : Scaling a linear symbol by a fraction blurs the module edges, and a symbol whose modules are no longer resolvable cannot be decoded however good the scanner is. Keeping the module size an integer number of pixels keeps every bar edge crisp. When even one pixel per module overflows the frame, the honest thing is to say so rather than hand out a weak image: For a decoder test, the same page exports the bare symbol with its quiet zone and no label furniture at all. ## Step 6: Print the Expected Result This is the part that turns a picture into a test. For the payload that is on screen, the page states: - the element string a decoder should return, with | marking each position where an FNC1 byte has to appear; - the human readable interpretation ; - every application identifier with the value the scanner should report, including the check-digit verdict; - and the GS1 Digital Link . A | only appears where a variable-length element needs terminating. Drawing one after every element would be easier to read and wrong: a fixed-length element needs no terminator, and showing one invites the reader to think it does. ### The preview is only as current as the last successful generation There is a failure mode here that is easy to ship and hard to notice: the preview and the expected-result table are produced after validation passes, so a payload with errors simply never reaches them — and whatever was drawn last stays on screen, looking current. That is worse than a blank canvas, because it invites someone to download or copy values that do not describe the payload in front of them. So when the payload has errors, everything downstream of generation is explicitly marked out of date in one place: The tabs deserve their own note. They used to look broken: the pressed state moved, but the early return above skipped both redraws, so clicking Symbol only changed nothing visible. A control that appears to respond and does not is worse than one that says it is unavailable, so they are disabled with a reason attached — and one click on the fix re-enables them, redraws the preview and restores the oracle together. ## Step 7: Close the Loop The generator is only worth having if the images actually round-trip. The verification harness drives both pages the way a person would: it renders the label in the generator, reads the PNG out of the canvas, uploads it to the GS1 scanner through the scanner’s own file input, and diffs every application identifier. Eleven scenarios, eleven matches, twice in a row with freshly randomised payloads — including the cases that only a real decoder can teach you: an ITF-14 that had been given a second check digit, a DataBar whose GTIN arrives with no AI, and a DataBar Expanded whose FNC1 separators the decoder dropped, letting the batch number swallow the serial that followed it. ## Scope and Limitations - GS1 Composite is not offered. bwip-js has no composite encoder, and a composite is anyway two symbols a reader has to associate. - DataBar Limited, ITF-14 and EAN-13 encode the GTIN and nothing else. The page reports the extra elements as an error rather than dropping them, and offers the symbology that can carry them. - A generated image is a fair test of decoding and parsing, and no test of authenticity. The payload is plaintext, so a parseable symbol proves only that it is well formed. Digitally signed carriers — an mDL, or the encrypted PDF417 on a South African driving licence — are the formats where a successful read says something about origin, and those cannot be generated from a web page at all. - Sample data only. Every value is synthetic and the preview is stamped SAMPLE . A GS1 barcode is not a secret and not an authority. ## Source Code Get the complete sample project source code on GitHub ### How to Build an Expo Barcode Scanner for Android and iOS URL: https://www.dynamsoft.com/codepool/expo-barcode-scanner.html Summary: A step-by-step tutorial for building an Expo (React Native) barcode scanner app that decodes 1D and 2D barcodes with the Dynamsoft Capture Vision native SDK through a hand-written Expo module — live camera scanning, photo-picker scanning, and result overlays with corner points. Author: Xiao Ling Published: 2026-09-04 Topics: Barcode Reader, QR Code, Barcode Scanner, Expo, React Native, TypeScript, Kotlin, Swift, Android, iOSScanning barcodes in an Expo app usually means installing a React Native plugin that wraps some native decoder. This sample takes a different route: the native bridge is written by hand inside the app as a local Expo module, so the app talks directly to the Dynamsoft Capture Vision native SDK on Android and iOS — no third-party scanning plugin in between. What you’ll build: An Expo SDK 57 app ( expo-barcode-scanner ) with a full-screen native camera scanner that decodes QR codes and other 1D/2D barcodes with the Dynamsoft Capture Vision template ReadBarcodes_Default , draws live overlays around every detected barcode, and returns a results list containing the decoded text, the barcode format, and the four corner points. A system photo-picker path decodes barcodes from existing images, and a scanFile method decodes from a local file path. ## Demo Video: Expo Barcode Scanner in Action ## Key Takeaways - You can call Dynamsoft’s native mobile SDKs from Expo without a third-party plugin: implement a local Expo module (Kotlin on Android, Swift on iOS) under the app’s modules/ folder and expose it to TypeScript with requireNativeModule . - The ReadBarcodes_Default template reads all supported 1D and 2D formats from live camera frames with no per-format configuration, and each decoded item comes with text, formatString , and a four-point location for drawing overlays. - Because activity results cannot be observed from an Expo module on Android, the module launches an invisible bridge activity that hosts startActivityForResult and settles the promise — a pattern that keeps the bridge promise-based and cancel-aware. - Camera, photo-picker, and file scanning share one native engine and one result type, so the React Native UI stays identical regardless of the image source. ## Common Developer Questions ### How do I decode barcodes in an Expo app with the Dynamsoft native SDK? Add a local Expo module that wraps the SDK and call three methods from JavaScript: initLicense() , then startScan() for the live camera scanner or scanFromGallery() / scanFile() for still images. The sample module is named ExpoDynamsoftBarcodeScanner and is implemented in ExpoDynamsoftBarcodeScannerModule.kt and .swift . ### What formats does the scanner support? The Dynamsoft Capture Vision ReadBarcodes_Default template covers 1D formats (Code 39, Code 128, Code 93, Codabar, EAN-13, EAN-8, ITF, UPC-A, UPC-E, and more) and 2D formats (QR Code, Data Matrix, PDF417, Aztec, and Micro QR), all in one scan session with no template tuning. ### What does each scan result contain? Each item in the resolved results array contains text (the decoded payload), formatString (for example QR_CODE or CODE_128 ), and points — four {x, y} corner locations of the barcode in the scanned frame. The React Native UI renders them as a list, and the native scanner draws matching overlays on the live preview. ### Can the same native code handle a photo-picker image? Yes. scanFromGallery() opens the system photo picker and decodes the selected image on the native side with the same engine and template; scanFile({ uri }) does the same for a path or content URI the app already holds. Both resolve with the identical ScanResult shape. ## Prerequisites Before you start, make sure you have the following: - Node.js (LTS) and npm for the Expo toolchain - Android Studio (latest) with JDK 17 and a connected Android device or emulator - Xcode (latest, macOS only) with a connected iPhone and a signing team configured in Xcode → Settings → Accounts - A Dynamsoft license key — the sample embeds a time-limited trial key that requires a network connection on first use Get a 30-day free trial license at dynamsoft.com/customer/license/trialLicense ## How the Sample Project Is Organized The complete app lives in the examples/expo-barcode-scanner folder of the sample repository: ## Step 1: Scaffold the Expo App and the Local Module Create the project with the blank TypeScript template and scaffold the module: If the scaffolder creates the module under modules/modules/ , move it up one level and delete the empty wrapper. Restrict expo-module.config.json to the native platforms and align the module class names: ## Step 2: Add Camera and Photo Library Permissions Declare the usage descriptions in app.json ; the Android module manifest adds the CAMERA and photo-library permissions that are merged into the app manifest: ## Step 3: Define the TypeScript API and the Result List UI The module surface declares the bridge methods and the result type. The location of every decoded barcode is delivered as four corner points: The home screen in App.tsx offers a Camera / Image File toggle, runs the same license-and-scan flow for both sources, and renders each decoded barcode as a card with its format, text, and corners: ## Step 4: Implement the Android Native Module The Android module adds the self-contained Dynamsoft Capture Vision bundle and appcompat (the scanner activity needs a LifecycleOwner for the camera): ScannerEngine.kt owns a single CaptureVisionRouter shared by the camera and file sources. The barcode preset template is used for both: ScannerActivity.kt is the full-screen scanner. A CapturedResultReceiver receives every decoded frame, enables the Confirm button as soon as at least one barcode is found, and draws the result quads on the DBR drawing layer of the camera view: Confirming serializes the items ( text , formatString , points ) to a JSON array and returns it as the activity result. As in the MRZ sample, BridgeResultActivity hosts the startActivityForResult call on behalf of the module and resolves the parked promise with the parsed JSON; a cancel produces a canceled rejection. ## Step 5: Implement the iOS Native Module The podspec depends on the self-contained Dynamsoft framework (never combine it with separate core or license pods — duplicate Objective-C classes break the camera preview): ExpoDynamsoftBarcodeScannerModule.swift presents BarcodeCameraScanViewController on the main queue. The controller binds a CameraView + CameraEnhancer to a CaptureVisionRouter , loads the barcode templates shipped inside the framework, and applies the decoded items to a custom green drawing layer in its CapturedResultReceiver : For the file data source the module runs a background decode with the same template and converts each item’s location points (wrapped as NSValue ) into plain {x, y} dictionaries: ## Step 6: Build and Run the App Self-contained release builds are produced with: Aim the camera at any barcode — the preview highlights every decoded barcode and the status bar shows the running count. Tap Confirm to open the results list with format, text, and corner coordinates: ## Common Issues & Edge Cases - Black camera preview on iOS when additional Dynamsoft pods are installed: DynamsoftCaptureVisionBundle already embeds the core and license modules. Adding DynamsoftCore or DynamsoftLicense separately registers duplicate Objective-C classes at launch — remove them and re-run pod install . - The app cannot run in Expo Go: the module registers custom activities and view controllers, so use npx expo run:android / npx expo run:ios or a release build. - “No barcodes were found in the image” on file scans: the image needs a complete, in-focus barcode. Damaged labels or extreme skew can fail; retry with a sharper capture. On the live camera, the Confirm button only enables after the first decode. - Cancel behavior: pressing back or Cancel rejects the promise with canceled ( RESULT_CANCELED on Android). The sample filters that message with /cancel/i instead of surfacing an error. - Trial license expires or the device is offline: the embedded key is time-limited and validates against Dynamsoft’s license server. Replace it in License.kt (Android) and in Constants.licenseKey of the Swift module before shipping. - Module name mismatches: requireNativeModule('ExpoDynamsoftBarcodeScanner') must match the class names and expo-module.config.json entries ( ExpoDynamsoftBarcodeScannerModule on both platforms); otherwise autolinking registers nothing and the import throws at startup. ## Source Code Get the complete sample project source code on GitHub The native bridge sources live in the modules/expo-dynamsoft-barcode-scanner folder of that project. For more on the underlying SDK, see the Dynamsoft Capture Vision documentation . ### How to Build an Ionic Vue QR Code Scanner with the Dynamsoft Capture Vision Native SDK URL: https://www.dynamsoft.com/codepool/ionic-vue-qr-code-scanner.html Summary: Build a cross-platform Ionic Vue QR code and barcode scanner powered by the Dynamsoft Capture Vision native SDK (capturevisionbundle 3.6.2000) on Android and iOS. Two data sources, no JavaScript SDK, real-time native overlay. Author: Xiao Ling Published: 2026-09-03 (updated 2026-09-03) Topics: Ionic, Capacitor, Android, iOS, QR Code, Barcode, Capture Vision, Native SDK, DataMatrix, PDF417Decoding QR codes and 1D barcodes from a phone camera needs a production-grade native pipeline. With the Dynamsoft Capture Vision native SDK ( com.dynamsoft:capturevisionbundle:3.6.2000 on Android, DynamsoftCaptureVisionBundle 3.6.2000 on iOS) wrapped in a small in-app Capacitor plugin, every frame is processed on-device with the multi-format preset template and the result is returned as JSON. This article walks through the ionic-vue-qr-code-scanner reference project — one Ionic Vue app, one in-app plugin, two data sources, and zero JavaScript scanning SDK. The scanner supports QR Code, DataMatrix, PDF417, Aztec, Code 39/93/128, EAN, UPC, ITF and Codabar, with real-time contour drawing on the native preview. The same code path accepts a still image from the gallery so a user can pick a saved photo and decode it with the same multi-format pipeline. ## What you’ll build - An Ionic Vue app that exposes two clearly separated data sources — Camera and File — with the live preview running entirely on the native side. - A multi-format barcode scanner that runs the built-in barcode preset template ( ReadBarcodes on Android, ReadBarcodes_Default after loading the SDK templates on iOS) on every camera frame and overlays the decoded contour through Dynamsoft’s DrawingLayer system on both platforms ( DBR_LAYER_ID on Android, a custom green-styled layer on iOS). - An in-app Capacitor plugin ( BarcodeScannerNative ) that returns every decoded barcode with text, format, format string and four corner points, ready to be rendered as a list. - A matching iOS Swift implementation that drops into the same Xcode project once the Dynamsoft pods are installed via pod install (see Step 4). ## Key Takeaways - Native SDK end-to-end. All decoding happens on-device. The WebView only renders the result; it never loads a JavaScript scanner. - Camera + file, one engine. Both data sources feed the same CaptureVisionRouter through the same ScannerEngine singleton so the deep-learning models load only once. - Latest Capture Vision release. Both platforms use the same 3.6.2000 SDK ( com.dynamsoft:capturevisionbundle and DynamsoftCaptureVisionBundle ). - No third-party plugin. The Capacitor bridge is fully implemented in this project. ## Architecture ## Common Developer Questions ### What barcode formats are supported? QR Code, DataMatrix, PDF417, Aztec, Code 39, Code 93, Code 128, EAN-8, EAN-13, UPC-A, UPC-E, ITF and Codabar. The ReadBarcodes preset template enables the entire set out of the box — no extra configuration is required. ### Should I add @capacitor/camera to handle the gallery picker? No. The native plugin implements its own gallery picker on Android (an ACTION_PICK intent through MediaStore ) and on iOS ( UIImagePickerController after requesting photo-library access). Adding @capacitor/camera would introduce a second picker that the user has to authorise separately. The reference project keeps the picker inside the plugin for exactly this reason. ### Why does the camera page not show an HTML video tag? DSCameraView (iOS) and CameraView (Android) are native views from the Camera Enhancer module. They run AVFoundation and CameraX respectively, which gives the scanner access to frame-accurate control and the drawing layer for real-time contour rendering. An HTML