Continued from Part 2
Developing Angular2 Dart Asset Services, Part 3
This article continues an effort to externalize content assets for a fictitious app. In Part 1 and Part 2, we developed an external HTML content repository and an Angular service to retrieve these assets. But our fictive scenario requires that we include not only external HTML but photo assets as well. In this article, we’ll provide that support in a similar strategy as was done for HTML content.
I’ll demonstrate how, relying on Dart and specifically Dart Angular’s strengths, we can easily automate much of the tedium of dealing with photo assets, including license compliance and converting raw images to production web assets.
Photo Service Interface
Just as with our HTML content provider, our interface of a Photo provider is simple:
abstract class PhotoService {
Future<Photo> getPhoto(PhotoId pId);
}
The getPhoto
method expects a PhotoId, which is simple container:
enum PhotoCategory { general, section, splash }
class PhotoId {
String id;
PhotoCategory type;
PhotoId(this.id, {this.type: PhotoCategory.general});
}
and returns a Future Photo:
class Photo {
PhotoId id;
Attribution attribution;
String url;
Photo(this.id, this.attribution, {this.url: null});
}
Image License Compliance
We know our app will need to use external images, but we also understand that most images are provided under license which may come with obligations on our part. Commonly authors must be attributed or licenses linked. Our app must abide by these rules, and therefore the image asset provider must supply this information to our app.
Hence, the Photo
class above contains an Attribution
, which dictates how attribution should be handled:
class Attribution {
String author;
String url;
String attributionText;
bool attributionRequired;
Attribution(this.author, { this.url: null,
this.attributionText: "", this.attributionRequired: false});
}
Note that information about the specific license is not included. Our app does not particularly care what the license is, only how any specific photo must be attributed. Later in the article, we’ll handle the translation of specific licenses into attribution details further down the stack (in fact, external to this app).
Client-based Photo Service
Our photo provider implementation needs to provide a Photo, meaning our implementation must determine the photo URL and attribution details. Starting with the URL part, we implement a simple solution:
@Injectable()
class ClientPhotoService implements PhotoService {
final Client _http;
final String _photoUrlPrefix = 'http://localhost:8082/content/';
ClientPhotoService();
@override
Future<Photo> getPhoto(PhotoId pId) async {
final Attribution attrib = null; // TODO
return new Photo(pId, attrib, url: _getPhotoUrl(pId.type, pId.id));
}
String _getPhotoUrl(PhotoCategory type, String id) =>
"${_photoUrlPrefix}images/${_categoryToPath(type)}/$id.jpg";
String _categoryToPath(PhotoCategory cat) =>
cat.toString().replaceFirst("PhotoCategory\.", "").toLowerCase();
}
Following the logic in getPhoto
, we are simply constructing a URL based on the provided photo identifier and category. There’s no need to physically fetch anything, since a web browser is very capable of fetching external image resources. We simply provide a URL and rely on our asset repository to conform to this structure.
We will have to fetch the licensing information via network request, however. We dictate a convention that our asset source include a JSON license file, with a .license
extension, for each photo. The format of the JSON is proscribed per the following example:
{
"author_url":"http://www.openfotos.com/view/waiting-terminal-4747","author":"cam",
"type":"PUBLIC_DOMAIN",
"attribution_text":"Photo by cam",
"attribution_required":false
}
With this convention defined, we update the ClientPhotoService
implementation to retrieve this information and instantiate the correct attribution details:
@Injectable()
class ClientPhotoService implements PhotoService {
final Client _http;
final String _photoUrlPrefix = 'http://localhost:8082/content/';
ClientPhotoService(this._http);
@override
Future<Photo> getPhoto(PhotoId pId) async {
final Attribution attrib = await _getPhotoAttribution(pId.type, pId.id);
return new Photo(pId, attrib, url: _getPhotoUrl(pId.type, pId.id));
}
Future<Attribution> _getPhotoAttribution(PhotoCategory type,
String id) async {
final Attribution attrib = new Attribution("Unknown");
final String url = "${_photoUrlPrefix}images/${_categoryToPath(
type)}/$id.jpg.license";
try {
final Response response = await _http.get(url);
if (response.statusCode == 200) {
final Map<String, dynamic> json = _extractData(response);
if (json != null) {
attrib.author = json['author'];
attrib.url = json['author_url'];
attrib.attributionText = json['attribution_text'];
attrib.attributionRequired = json['attribution_required'];
}
return attrib;
} else {
_handleError(url, "Response ${response.statusCode}");
return attrib;
}
} catch (e) {
_handleError(url, e.runtimeType);
return attrib;
}
}
String _getPhotoUrl(PhotoCategory type, String id) =>
"${_photoUrlPrefix}images/${_categoryToPath(type)}/$id.jpg";
String _categoryToPath(PhotoCategory cat) =>
cat.toString().replaceFirst("PhotoCategory\.", "").toLowerCase();
dynamic _extractData(Response resp) =>
JSON.decode(UTF8.decode(resp.bodyBytes));
void _handleError(String url, dynamic e) {
//TODO handle error
}
}
The implementation of _getPhotoAttribution
is similar to the ClientContentService
implementation of getContent
discussed in
Part 2. We fetch a remote file, in this case the JSON license file, using an injected Client. Instead of sanitizing response as we did for HTML content, we simply decode the JSON and assign Attribution
fields parameters from the decoded data.
Our app is now free to inject and use this photo asset provider. Our app will decide exactly how to properly attribute photos based on the simple Attribution instructions provided from the service.
An External Photo Repository
While technically we’ve completed our obligations to our app, we don’t actually have any photo assets to show or test with. We can construct alternate providers for use in testing (there are a few in the repo), but we can also make our own photo repo for use with ClientPhotoService
. However, resizing and cropping photos and hand-writing JSON certainly isn’t our style, so what can an impatient Dart author do?
Recall in Part 2 we built a HTML content repo that would allow copywriters to author in Markdown. Our content repo, with some custom transformers, would automatically convert these markdown files into production-ready HTML. We’ll use a similar approach here, allowing our photographers to simply store raw images and specify basic licensing info. Our repo will take care of the work of transforming these into production-ready assets.
To start, we create a new repo and define our photo assets with all pertinent info in a yaml file assets/image/section_photo.yaml
. An excerpt:
photos:
- kittens:
in_filename: section/kitten_2017_02_23.png
out_filename: kittens.jpg
license:
type: GFDL
author: Bart Dartner
author_url: https://dartlang.org
- doggo:
in_filename: section/d.jpg
out_filename: doggo.jpg
license:
type: CC_BY_SA_30
author: Dart Bartner
author_url: https://google.com
Here we’ve described two input photo assets, along with our their desired output name and any pertinent licensing info.
We now define the licenses used above, including the necessary attribution requirements for each. Example:
abstract class License {
LicenseType get type;
String getAttribution(String author);
bool get attributionRequired => true;
}
class CcBySa30License extends License {
LicenseType get type => LicenseType.LICENSE_CC_BY_SA_30;
String getAttribution(String author) => "Photo by $author";
bool get attributionRequired => true;
}
There are additional licenses and some helpers, such as a LicenseFactory
, that I omit here for clarity. See source for more detail.
Finally, we create a Dart bin script to generate our production assets and license JSON from the above yaml. Our script will loop through each entry in the yaml, for each performing cropping/scaling/resizing using the ImageMagic convert binary and writing the JSON license file.
// constants omitted for readability
main() async {
String inPath = path.absolute(INPATH);
String outPath = path.absolute(OUTPATH);
Directory outDir = new Directory(outPath);
if (!outDir.existsSync()) {
await outDir.create(recursive: true);
}
String contents = await new File(YAML_PATH).readAsString();
var yaml = loadYaml(contents);
Directory tmpDir = await Directory.systemTemp.createTemp(tmpSubdirName);
await Future.forEach(yaml['photos'], (Map f) async {
print("Converting $INPATH${f['in_filename']} " +
"to $OUTPATH${f['out_filename']}");
String iFile = path.join(inPath, f['in_filename']);
String oFile = path.join(outPath, f['out_filename']);
String tmpBaseName = path.basenameWithoutExtension(iFile);
String tmpFileName = "${tmpDir.path}/$tmpBaseName.cache.jpg";
new File(iFile).copySync(tmpFileName); //copy to tmp location
await doImageConvert(CONVERT_SCRIPT, ['-quality', '100'], tmpFileName);
await doImageConvert(CONVERT_SCRIPT,
['-thumbnail', FINAL_GEOMETRY, '-quality', '$QUALITY'], tmpFileName);
new File(tmpFileName).copySync(oFile); //copy to final location
writeLicenseFile(f['license'], "$oFile.license");
});
tmpDir.delete(recursive: true);
}
Future<Null> doImageConvert(final String cmd, final List<String> args,
final String filename) async {
List<String> thisArgs = new List.from(args)
..add(filename)..add(filename);
await exitOnFail(Process.run(cmd, thisArgs));
return;
}
Future<Null> writeLicenseFile(Map<String, String> licenseAttribs,
String fName) async {
License l = new LicenseFactory().getLicenseFromString(licenseAttribs['type']);
Map<String, dynamic> out = {}..addAll(licenseAttribs);
out['attribution_text'] = l.getAttribution(licenseAttribs['author']);
out['attribution_required'] = l.attributionRequired;
await new File(fName).create()
..writeAsString(JSON.encode(out));
}
Future<Null> exitOnFail(Future<ProcessResult> resF) async {
ProcessResult res = await resF;
if (res.exitCode != 0) {
stderr.writeln(res.stderr);
exit(res.exitCode);
}
return;
}
The example above is fairly simple, but the script could be updated to apply effects, watermark images, modify image metadata, etc.
Now, if we place source images in assets/photo/section
and update our section_photos.yaml
accordingly, then when we run pub bin/gen_section_photos
our processed images and JSON license info will be deposited into the web/content/images/section
directory, just like our photo asset provider expects.
We can then pub serve
this repo for development use or, for production, simply copy our generated assets to a static web server.
Conclusion
In these three articles, I’ve illustrated how relying on the strengths of Angular Dart and Dart in general can greatly boost productivity and flexibility.
Dart and Angular Dart’s features, including dependency injection, futures, transformers, and robust web-centric libraries make web development fun again.
Source
These articles were based on work I did creating inTallinn, a visitors’ guide to my home city Tallinn, Estonia. See the full source code of inTallinn, which expands on the contents of these articles.