Binarize
From Wiktionary:
Etymology
binary + size
Verb
binarize (third-person singular simple present binarizes, present participle binarizing, simple past and past participle binarized)
- (mathematics) To represent in binary (base 2) notation
- To convert (an image) to only black and white.
- (statistics) To dichotomize a variable.
Binarize is a package that wraps the ByteData functionality into a more streamlined and extendable payload system.
With Binarize you can easily create and read binary data into the correct value types without having to check for the correct byte offset or figure out which ByteData method you need to use.
This is especially useful for when you are implementing a file format specification, like the PSD File Format.
Installation
Add binarize
as a dependency to your pubspec.yaml file (what?).
Import Binarize:
import 'package:binarize/binarize.dart';
Docs & API
The API Docs provide information about how to use Binarize.
If you want to see Binarize in practice, check the example. It showcases all the types that Binarize provides.
Usage
Writing
The Payload.write
method returns a PayloadWriter
that is used for writing values using
different PayloadTypes:
import 'package:binarize/binarize.dart';
void main() {
final writer = Payload.write()
..set(uint8, 16);
..set(string32, 'Hello World');
}
The binarize
method accepts a PayloadWriter
and converts the values to binary:
void main() {
final writer = Payload.write();
...
// Binarize the writer to a Uint8List.
final bytes = binarize(writer);
...
}
Reading
The Payload.read
method returns a PayloadReader
that reads a list of bytes. The data from that
list can be retrieved by using different PayloadTypes:
import 'package:binarize/binarize.dart';
void main() {
...
final reader = Payload.read(bytes);
final aUint8 = reader.get(uint8);
final aString = reader.get(string32);
...
}
Note: The order in which the values are written is also the order in which they have to be retrieved.
PayloadType
s
PayloadType
s are the backbone of Binarize, they do all the heavy lifting of packing and unpacking
bytes. Binarize provides all the basis types that the ByteData class has to offer. But in some
cases a developer might want to create their own PayloadType
. Thankfully Binarize's API allows
for defining custom PayloadType
s.
Here is an example for a PayloadType
that packs and unpacks the Vector2
class from the
vector_math library:
import 'package:binarize/binarize.dart';
import 'package:vector_math/vector_math.dart';
// Define the PayloadType class, it is private as we only want to have a
// single constant instance for it.
class _Vector2 extends PayloadType<Vector2> {
const _Vector2();
// The byte length, it should return the total byte length it will use for a
// given value. In this case we have two float32 values, each uses 4 bytes
// so our total length is 8.
@override
int length(Vector2 value) => 8;
// Called when the user reads data, the offset is the current read offset
// in the given data.
@override
Vector2 get(ByteData data, int offset) {
return Vector2(
// Read the x value at the current offset.
data.getFloat32(offset),
// Read the y value at the current offset + 4. Float32 has a default
// byte length of 4 so we have to take that into consideration so
// that we don't read the x value again.
data.getFloat32(offset + 4),
);
}
// Called when the user write data, the offset is the current write offset
// in the given byte data.
@override
void set(Vector2 value, ByteData data, int offset) {
// Write the x value at the current offset.
data.setFloat32(offset, value.x);
// Write the y value at the current offset + 4. The 4 is needed as Float32
// have a byte length of 4 so we have to take that into consideration so
// we don't overwrite the x value.
data.setFloat32(offset + 4, value.y);
}
}
// And finally the user-facing variable that users can use to read and write
// with.
const vector2 = _Vector2();
The vector2
can be used like any other PayloadType
:
import 'package:binarize/binarize.dart';
import 'package:vector_math/vector_math.dart';
// Import the newly created vector2 PayloadType.
import './types/vector2.dart';
void main() {
// Write a Vector2.
final writer = Payload.write()..set(vector2, Vector2(100.5, 200));
// Binarize the writer to a Uint8List.
final bytes = binarize(writer);
// Reading the bytes.
final reader = Payload.read(bytes);
// The x value will be 100.5 and the y value will be 200.
final aVector2 = reader.get(vector2);
}
BinaryContract
The BinaryContract
allows developers to define a strongly typed binary
representation of a Dart class. If your Dart class contract changes, so will
the binary contract, ensuring you never forget to change how the data is being
read or stored.
BinaryContract
works best with classes that are immutable, because it will
implement your class by overwriting the fields as getters. If your fields
are not final
Dart will complain that the setter was not implement. There
are ways to work around this but it is not recommended.
An example of how you can implement a BinaryContract
for your own classes:
class MyClass {
const MyClass({
required this.myProperty,
});
final int myProperty;
}
/// Defining the contract for the [MyClass] class. It should also implement the
/// class so the contract can access the fields.
class _MyClassContract extends BinaryContract<MyClass> implements MyClass {
/// Pass a base line instance to the super constructor. This is used to build
/// up the, contact, the values defined in the base line do not matter.
const _MyClassContract()
: super(const MyClass(myProperty: 1337));
/// This method allows the [BinaryContract] to know the order of how the binary
/// data should be read and stored.
///
/// [contract] is always the [BinaryContract] instance.
@override
MyClass order(MyClass contract) {
return MyClass(
myProperty: contract.myProperty,
);
}
/// The properties of the [MyClass] need to be implemented as getters.
@override
int get myProperty => type(uint16, (o) => o.myProperty);
}
// Create a constant instance of the contract.
const myClassContract = _MyClassContract();
void main() {
// Create a new instance of MyClass.
final myClass = MyClass(myProperty: 1337);
// Store the instance using the contract.
final writer = Payload.write()..set(myClassContract, myClass);
final bytes = binarize(writer);
// Read it back from binary.
final reader = Payload.read(bytes);
final myClassAgain = reader.get(myClassContract);
print(myClassAgain.myProperty); // Returns 1337
}
The BinaryContract
extends from PayloadType
and can be used within other payloads, including
itself.
Libraries
- binarize
- Binarize allows for a more streamlined and extendable binary creation experience.