Skip to main content

niimbot_print 0.3.2 copy "niimbot_print: ^0.3.2" to clipboard
niimbot_print: ^0.3.2 copied to clipboard

Compose, preview, and print text, codes, images, lines, and pixel data with Niimbot printers on Android and iOS.

Niimbot Print #

A Flutter plugin for discovering, connecting to, and printing text labels or QR codes with supported Niimbot Bluetooth printers on Android and iOS.

Features #

  • Scan for nearby Niimbot printers.
  • Filter scan results by supported printer model.
  • Connect and disconnect over Bluetooth.
  • Compose text, QR/matrix codes, barcodes, images, and lines in one request.
  • Load images from bytes, files, URLs, or raw grayscale pixels.
  • Generate native PNG previews before printing.
  • Check the current printer connection state.

Supported platforms #

  • Android (minimum SDK 19)
  • iOS 12.0 or later

The package requires Flutter 3.44 or later.

This release bundles Niimbot Android SDK 4.1.1, image SDK 1.9.5, and iOS JCAPI SDK 3.2.8. Applications do not need to add these native SDK files separately.

Bluetooth printing should be tested on a physical device. Bluetooth features and the bundled native Niimbot SDK might not work in a simulator or emulator.

Supported printer models #

The currently available model filters are:

  • B1
  • B3S
  • B21
  • ZZ401

Installation #

Add niimbot_print to your pubspec.yaml:

dependencies:
  niimbot_print: ^0.3.2

Then install the dependency:

flutter pub get

Platform setup #

iOS #

Add the Bluetooth usage descriptions to ios/Runner/Info.plist:

<key>NSBluetoothAlwaysUsageDescription</key>
<string>This app requires Bluetooth access to connect to Niimbot printers.</string>
<key>NSBluetoothPeripheralUsageDescription</key>
<string>This app requires Bluetooth access to communicate with Niimbot printers.</string>

Enable the Bluetooth permission used by permission_handler in the host application's ios/Podfile:

post_install do |installer|
  installer.pods_project.targets.each do |target|
    flutter_additional_ios_build_settings(target)
    target.build_configurations.each do |config|
      config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
        '$(inherited)',
        'PERMISSION_BLUETOOTH=1',
      ]
    end
  end
end

After changing the Podfile, run pod install from the application's ios directory. If Bluetooth permission was previously denied, enable it from iOS Settings or reinstall the application so iOS can request it again.

The plugin includes the required native iOS libraries. No additional SDK installation is required.

Android #

The required Bluetooth permissions and native Android libraries are included by the plugin and merged into the application automatically. On Android 11 and earlier, the system can request location permission because classic Bluetooth discovery requires it. Android 12 and later uses the Nearby devices permission.

Usage #

Import the package and create a NiimbotPrint instance:

import 'package:niimbot_print/niimbot_print.dart';

final niimbotPrint = NiimbotPrint();

Scan for printers #

Use whiteListDevices to return only specific supported models. Omit it to return all discovered Bluetooth devices.

final devices = await niimbotPrint.onStartScan(
  scanDuration: const Duration(seconds: 6),
  whiteListDevices: const [
    NiimbotModelEnum.b1,
    NiimbotModelEnum.b21,
  ],
  onError: (message) {
    print('Scan failed: $message');
  },
);

if (devices.isEmpty) {
  print('No Niimbot printer found.');
}

The plugin requests the required Bluetooth runtime permissions when scanning, connecting, or printing. If permission is denied or Bluetooth is disabled, the error callback receives an explanatory message.

Connect to a printer #

Pass one of the devices returned by onStartScan:

if (devices.isNotEmpty) {
  await niimbotPrint.onStartConnect(
    model: devices.first,
    onResult: (isSuccess, message) {
      print(isSuccess ? 'Connected: $message' : 'Connection failed: $message');
    },
  );
}

You can check the connection at any time:

final connected = await niimbotPrint.isConnected();

One print request can combine text, QR codes, barcodes, images, and lines.

await niimbotPrint.onStartPrint(
  layout: const NiimbotLabelLayout(
    // Presets: mm50x30, mm50x80, and mm50x170.
    // Any positive dimensions can also be supplied with
    // NiimbotLabelSize.custom(width: 50, height: 120).
    size: NiimbotLabelSize.mm50x80,
    orientation: NiimbotLabelOrientation.landscape,
    horizontalAlignment: NiimbotLabelAlignment.center,
    verticalAlignment: NiimbotLabelAlignment.center,
    padding: 2,
    textSpacing: 1,
  ),
  elements: [
    PrintLabelModel(
      text: 'Product name',
      font: NiimbotFontFamily.defaultFont,
      style: NiimbotFontStyle.bold,
      alignment: NiimbotTextAlignment.center,
      sizing: const NiimbotTextSizing.autoFit(
        minFontSize: 10,
        maxFontSize: 36,
        maxLines: 2,
        fontSizeOffset: 0,
      ),
    ),
    PrintLabelModel(
      text: 'SKU-0001',
      sizing: const NiimbotTextSizing.fixed(14),
    ),
  ],
  onResult: (isSuccess, message) {
    print(isSuccess ? 'Print succeeded: $message' : 'Print failed: $message');
  },
);

Label dimensions are expressed in millimetres. Landscape mode rotates the logical canvas while preserving the physical media dimensions. A text item can use either an exact font size with NiimbotTextSizing.fixed or select the largest size within a range using NiimbotTextSizing.autoFit. Font families are resolved by the native Niimbot SDK and must be present in the platform's font resources; unsupported names fall back according to the SDK.

Each text item can independently use NiimbotFontStyle.normal, bold, italic, boldItalic, underline, or strikethrough. Font style is optional and defaults to normal, so existing print requests remain compatible.

Auto-fit items on the same label share one calculated base size. Set fontSizeOffset to create a hierarchy such as 0, -2, and -4 while keeping every text item within its configured bounds. Long text is wrapped at word boundaries up to maxLines; the renderer then centers the complete multi-line text block using the layout alignment. Font size is reduced only when the wrapped content still cannot fit the available label area. Use textSpacing to add millimetre-based spacing between automatic-flow text elements without changing the spacing between wrapped lines of one element. Each PrintLabelModel may override the layout alignment with NiimbotTextAlignment.left, center, or right. When omitted, text follows the label's horizontalAlignment. Centered automatic text flow measures the native preview's visible ink bounds before the final render, balancing the horizontal margins and vertically centering the visible text block. If the SDK cannot generate the measurement preview, frame alignment is used. Physical media positioning still depends on the printer's calibration and the configured label size matching the sticker. For automatic text flow, set horizontalCalibration in NiimbotLabelLayout to compensate for media positioning (negative moves left, positive moves right). It defaults to zero and must not exceed padding in either direction. Preview includes this correction intentionally; it is applied after centering and does not affect font sizing or wrapping. Calibration follows the logical canvas horizontal axis, so check it again when changing orientation or media.

An internal device-specific profile automatically corrects centered text on device B1-F729063519 with 50 x 30 mm portrait media by -2 mm. No manual setting is needed for that combination. The profile is device-specific and does not claim to detect the physical sticker position on other printers. Nonzero explicit calibration overrides the profile to avoid double correction.

Profiles now come from the same Dart configuration on Android and iOS, instead of being hardcoded into either renderer. Disable built-ins with usePrinterProfiles: false, or replace the list with measured profiles:

const layout = NiimbotLabelLayout(
  printerProfiles: [
    NiimbotPrinterMediaProfile(
      printerName: 'MY-PRINTER',
      size: NiimbotLabelSize.mm50x30,
      horizontalCorrection: -1,
    ),
  ],
);

Profiles match the exact printer name (case-insensitive), media dimensions, orientation, and centered text flow. Corrections exceeding the configured padding are ignored. No matching profile means no physical media correction. Use horizontalCalibration for an explicit nonzero override, or disable profiles to request zero correction. Profiles affect automatic text flow only.

Successful ink measurements are cached for identical text, font/style, layout, and print resolution. Both platforms keep at most 32 hash/coordinate entries in memory; no attendee text, image, or label JSON is retained in this cache. Failed/blank probes are retried. Native logs include cacheHit and elapsedMs so cold and warm renders can be compared on the actual device.

Wrapping still uses a conservative character-width estimate; ink centering uses the actual SDK preview. The bundled SDK does not expose a public glyph measurement API, so exact font-aware wrapping is not claimed. Native geometry tests do not replace physical printer tests.

Run the native geometry tests alongside flutter test:

cd example/android
./gradlew :niimbot_print:testDebugUnitTest

The Foundation-only Objective-C runner at ios/Tests/RenderGeometryTests.m can also be compiled on macOS with InkBounds.m and PrinterMediaProfile.m using clang -fobjc-arc -framework Foundation -Iios/Classes.

QR and matrix codes can use an explicit frame, or let the plugin calculate their size and position. Both width and height are optional. When only one is supplied, the other dimension uses the same value so the code stays square. When both are omitted, the plugin chooses a square that fits the label.

await niimbotPrint.onStartPrint(
  elements: const [
    PrintQrCodeModel(
      data: 'https://example.com/products/SKU-0001',
      width: 22, // height is inferred as 22 mm
      horizontalAlignment: NiimbotElementAlignment.end,
      verticalAlignment: NiimbotElementAlignment.center,
      rotation: NiimbotRotation.degree90,
    ),
  ],
  onResult: (isSuccess, message) {
    print(isSuccess
        ? 'QR code printed: $message'
        : 'QR code print failed: $message');
  },
);

To position a code at exact coordinates instead, provide frame. An explicit frame takes precedence over automatic dimensions and alignment.

Barcode dimensions, alignment, and rotation work the same way. If width is omitted, it fills the available label width. If height is omitted, the plugin chooses a readable height based on the label.

await niimbotPrint.onStartPrint(
  elements: const [
    NiimbotBarcodeElement(
      data: '123456789012',
      height: 12, // width is selected automatically
      type: NiimbotBarcodeType.code128,
      horizontalAlignment: NiimbotElementAlignment.center,
      verticalAlignment: NiimbotElementAlignment.end,
      rotation: NiimbotRotation.degree0,
    ),
  ],
  onResult: (success, message) {},
);

Available element alignments are start, center, and end, independently for the horizontal and vertical axes. Rotation uses the safe NiimbotRotation.degree0, degree90, degree180, or degree270 enum and defaults to degree0.

Automatic alignment positions an individual code; it is not a flow layout. When two automatically positioned codes would overlap, validation fails before calling the native SDK. Select different alignments or provide explicit frames when composing multiple codes.

Text-only labels may omit frames and use automatic vertical flow. Once text is mixed with a positioned code, image, or line, every text element must also have an explicit frame so the canvas layout remains deterministic.

Images require a frame and can come from a network URL, local file, or bytes:

final networkImage = await NiimbotImageElement.fromNetwork(
  uri: Uri.parse('https://example.com/logo.png'),
  frame: const NiimbotRect(x: 2, y: 2, width: 20, height: 20),
);

final fileImage = await NiimbotImageElement.fromFile(
  path: '/path/to/logo.png',
  frame: const NiimbotRect(x: 2, y: 2, width: 20, height: 20),
);

final bytesImage = NiimbotImageElement.bytes(
  bytes: imageBytes,
  frame: const NiimbotRect(x: 2, y: 2, width: 20, height: 20),
);

await niimbotPrint.onStartPrint(
  elements: [networkImage], // or fileImage / bytesImage
  onResult: (success, message) {},
);

Provide exactly pixelWidth * pixelHeight grayscale bytes. Values below 128 print black and values of 128 or more print white:

final pixels = Uint8List.fromList(List<int>.generate(
  96 * 48,
  (index) => ((index % 96) ~/ 8 + (index ~/ 96) ~/ 8).isEven ? 0 : 255,
));

final pixelImage = NiimbotImageElement.pixels(
  pixelWidth: 96,
  pixelHeight: 48,
  pixels: pixels,
  frame: const NiimbotRect(x: 5, y: 3, width: 40, height: 24),
);
const line = NiimbotLineElement(
  frame: NiimbotRect(x: 3, y: 14, width: 44, height: 0.8),
  style: NiimbotLineStyle.dashed,
  dashLength: 1,
  gapLength: 1,
  rotation: NiimbotRotation.degree0,
);

Combine multiple elements #

All element types can be sent in the same onStartPrint call. Frames and dimensions are expressed in millimetres.

final logo = await NiimbotImageElement.fromNetwork(
  uri: Uri.parse('https://example.com/logo.png'),
  frame: const NiimbotRect(x: 2, y: 2, width: 12, height: 12),
);

await niimbotPrint.onStartPrint(
  layout: const NiimbotLabelLayout(size: NiimbotLabelSize.mm50x80),
  elements: [
    logo,
    const NiimbotBarcodeElement(
      data: '1234567890',
      width: 34,
      height: 10,
      horizontalAlignment: NiimbotElementAlignment.start,
      verticalAlignment: NiimbotElementAlignment.end,
      type: NiimbotBarcodeType.code128,
    ),
    const NiimbotLineElement(
      frame: NiimbotRect(x: 2, y: 36, width: 46, height: 0.5),
    ),
  ],
  onResult: (success, message) {},
);

Generate a print preview #

The returned PNG is generated by the native Niimbot rendering SDK and can be displayed directly by Flutter:

final png = await niimbotPrint.generatePreview(
  layout: const NiimbotLabelLayout(size: NiimbotLabelSize.mm50x80),
  elements: const [
    PrintQrCodeModel(
      data: 'https://example.com',
      horizontalAlignment: NiimbotElementAlignment.center,
      verticalAlignment: NiimbotElementAlignment.center,
    ),
  ],
);

final preview = Image.memory(png);

Handle validation errors #

Printing validates the label, element bounds, QR dimensions, common barcode formats, and automatic-code collisions before invoking Bluetooth or native rendering. Use onError when the application needs a stable error code:

await niimbotPrint.onStartPrint(
  elements: elements,
  onError: (error) {
    print(error.code);         // NiimbotPrintErrorCode
    print(error.elementIndex); // null or the invalid element index
    print(error.message);
  },
  onResult: (success, message) {},
);

generatePreview throws NiimbotPrintException for validation and preview failures, so it can be handled with a normal try/catch block.

Disconnect #

final disconnected = await niimbotPrint.onDisconnect();

Complete example #

See the example directory for a complete Flutter application that demonstrates scanning, connecting, configurable label sizes and orientation, optional code dimensions, alignment, rotation, previewing, and printing every supported element type.

Author #

2
likes
140
points
507
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Compose, preview, and print text, codes, images, lines, and pixel data with Niimbot printers on Android and iOS.

Homepage
Repository (GitHub)
View/report issues

Topics

#bluetooth #printer #label-printer #qr-code #niimbot

License

MIT (license)

Dependencies

flutter, flutter_blue_plus, permission_handler, plugin_platform_interface

More

Packages that depend on niimbot_print

Packages that implement niimbot_print