David PHAM-VAN

Uses an internal TTF parser

... ... @@ -9,3 +9,5 @@ pubspec.lock
# Directory created by dartdoc
# If you don't generate documentation locally you can remove this line.
doc/api/
*.pdf
... ...
# 1.0.3
* Remove dependency to ttf_parser
# 1.0.2
* Update sdk support for 2.0.0
# 1.0.1
* Add example
* Lower vector_math dependency version
* Uses better page format object
... ...
# Pdf creation library for dart / flutter
This is a low-level Pdf creation library.
It can create a full multi-pages document with graphics,
images and text using TrueType fonts.
The coordinate system is using the internal Pdf system:
* (0.0, 0.0) is bottom-left
* 1.0 is defined as 1 / 72.0 inch
* you can use the constants for centimeters, milimeters and inch defined in PDFPageFormat
Example:
```dart
final pdf = new PDFDocument();
... ... @@ -12,8 +21,30 @@ g.drawRect(50.0, 30.0, 100.0, 50.0);
g.fillPath();
g.setColor(new PDFColor(0.3, 0.3, 0.3));
g.drawString(font, 12.0, "Hello World!", 50.0, 300.0);
g.drawString(font, 12.0, "Hello World!", 5.0 * PDFPageFormat.MM, 300.0);
var file = new File('file.pdf');
file.writeAsBytesSync(pdf.save());
```
To load an image it is possible to use the dart library `image`
```dart
Image image = decodeImage(new Io.File('test.webp').readAsBytesSync());
PDFImage image = new PDFImage(
pdf,
image: img.data.buffer.asUint8List(),
width: img.width,
height: img.height);
g.drawImage(image, 100.0, 100.0, 80.0);
```
To use a TrueType font:
```dart
PDFTTFFont ttf = new PDFTTFFont(
pdf,
(new File("open-sans.ttf").readAsBytesSync() as Uint8List).buffer.asByteData());
g.setColor(new PDFColor(0.3, 0.3, 0.3));
g.drawString(ttf, 20.0, "Dart is awesome", 50.0, 30.0);
```
... ...
... ... @@ -23,7 +23,6 @@ import 'dart:io';
import 'dart:typed_data';
import 'package:meta/meta.dart';
import 'package:ttf_parser/ttf_parser.dart';
import 'package:vector_math/vector_math_64.dart';
part 'src/annotation.dart';
... ... @@ -50,6 +49,7 @@ part 'src/point.dart';
part 'src/polygon.dart';
part 'src/rect.dart';
part 'src/stream.dart';
part 'src/ttf_parser.dart';
part 'src/ttffont.dart';
part 'src/xobject.dart';
part 'src/xref.dart';
... ...
... ... @@ -20,11 +20,9 @@ part of pdf;
class PDFFontDescriptor extends PDFObject {
final PDFObjectStream file;
final TtfFont font;
final PDFTTFFont ttfFont;
PDFFontDescriptor(this.ttfFont, this.file, this.font)
: super(ttfFont.pdfDocument, "/FontDescriptor");
PDFFontDescriptor(this.ttfFont, this.file) : super(ttfFont.pdfDocument, "/FontDescriptor");
@override
void prepare() {
... ... @@ -34,9 +32,10 @@ class PDFFontDescriptor extends PDFObject {
params["/FontFile2"] = file.ref();
params["/Flags"] = PDFStream.intNum(32);
params["/FontBBox"] = new PDFStream()
..putStringArray([font.head.xMin, font.head.yMin, font.head.xMax, font.head.yMax]);
params["/Ascent"] = PDFStream.intNum(font.hhea.ascent);
params["/Descent"] = PDFStream.intNum(font.hhea.descent);
..putStringArray(
[ttfFont.font.xMin, ttfFont.font.yMin, ttfFont.font.xMax, ttfFont.font.yMax]);
params["/Ascent"] = PDFStream.intNum(ttfFont.font.ascent);
params["/Descent"] = PDFStream.intNum(ttfFont.font.descent);
params["/ItalicAngle"] = PDFStream.intNum(0);
params["/CapHeight"] = PDFStream.intNum(10);
params["/StemV"] = PDFStream.intNum(79);
... ...
/*
* Copyright (C) 2018, David PHAM-VAN <dev.nfet.net@gmail.com>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
part of pdf;
class TTFParser {
static const _HEAD = "head";
static const _NAME = "name";
static const _HMTX = "hmtx";
static const _HHEA = "hhea";
static const _CMAP = "cmap";
static const _MAXP = "maxp";
static const _LOCA = "loca";
static const _GLYF = "glyf";
final ByteData bytes;
final _tableOffsets = new Map<String, int>();
String _fontName;
final advanceWidth = new List<double>();
final charToGlyphIndexMap = new Map<int, int>();
final glyphOffsets = new List<int>();
final glyphInfoMap = new Map<int, PDFRect>();
TTFParser(this.bytes) {
final numTables = bytes.getUint16(4);
for (var i = 0; i < numTables; i++) {
final name = utf8.decode(bytes.buffer.asUint8List(i * 16 + 12, 4));
final offset = bytes.getUint32(i * 16 + 20);
_tableOffsets[name] = offset;
}
_parseFontName();
_parseHmtx();
_parseCMap();
_parseIndexes();
_parseGlyf();
}
get unitsPerEm => bytes.getUint16(_tableOffsets[_HEAD] + 18);
get xMin => bytes.getInt16(_tableOffsets[_HEAD] + 36);
get yMin => bytes.getInt16(_tableOffsets[_HEAD] + 38);
get xMax => bytes.getInt16(_tableOffsets[_HEAD] + 40);
get yMax => bytes.getInt16(_tableOffsets[_HEAD] + 42);
get indexToLocFormat => bytes.getInt16(_tableOffsets[_HEAD] + 50);
get ascent => bytes.getInt16(_tableOffsets[_HHEA] + 4);
get descent => bytes.getInt16(_tableOffsets[_HHEA] + 6);
get numOfLongHorMetrics => bytes.getInt16(_tableOffsets[_HHEA] + 34);
get numGlyphs => bytes.getInt16(_tableOffsets[_MAXP] + 4);
get fontName => _fontName;
void _parseFontName() {
final basePosition = _tableOffsets[_NAME];
final count = bytes.getUint16(basePosition + 2);
final stringOffset = bytes.getUint16(basePosition + 4);
int pos = basePosition + 6;
for (var i = 0; i < count; i++) {
int platformID = bytes.getUint16(pos);
int nameID = bytes.getUint16(pos + 6);
int length = bytes.getUint16(pos + 8);
int offset = bytes.getUint16(pos + 10);
pos += 12;
if (platformID == 1 && nameID == 6) {
_fontName = utf8
.decode(bytes.buffer.asUint8List(basePosition + stringOffset + offset, length));
}
}
}
void _parseHmtx() {
final offset = _tableOffsets[_HMTX];
final unitsPerEm = this.unitsPerEm;
for (var i = 0; i < numOfLongHorMetrics; i++) {
advanceWidth.add(bytes.getInt16(offset + i * 4).toDouble() / unitsPerEm);
}
}
void _parseCMap() {
final basePosition = _tableOffsets[_CMAP];
final numSubTables = bytes.getUint16(basePosition + 2);
for (var i = 0; i < numSubTables; i++) {
final offset = bytes.getUint32(basePosition + i * 8 + 8);
final format = bytes.getUint16(basePosition + offset);
final length = bytes.getUint16(basePosition + offset + 2);
switch (format) {
case 0:
_parseCMapFormat0(basePosition + offset + 4, length);
break;
case 4:
_parseCMapFormat4(basePosition + offset + 4, length);
break;
case 6:
_parseCMapFormat6(basePosition + offset + 4, length);
break;
}
}
}
void _parseCMapFormat0(int basePosition, int length) {
assert(length == 262);
for (var i = 0; i < 256; i++) {
int charCode = i;
int glyphIndex = bytes.getUint8(basePosition + i);
if (glyphIndex > 0) {
charToGlyphIndexMap[charCode] = glyphIndex;
}
}
}
void _parseCMapFormat4(int basePosition, int length) {
final segCount = bytes.getUint16(basePosition + 2) ~/ 2;
final endCodes = new List<int>();
for (var i = 0; i < segCount; i++) {
endCodes.add(bytes.getUint16(basePosition + i * 2 + 10));
}
final startCodes = new List<int>();
for (var i = 0; i < segCount; i++) {
startCodes.add(bytes.getUint16(basePosition + (segCount + i) * 2 + 12));
}
final idDeltas = new List<int>();
for (var i = 0; i < segCount; i++) {
idDeltas.add(bytes.getUint16(basePosition + (segCount * 2 + i) * 2 + 12));
}
final idRangeOffsetBasePos = basePosition + segCount * 6 + 12;
final idRangeOffsets = new List<int>();
for (var i = 0; i < segCount; i++) {
idRangeOffsets.add(bytes.getUint16(idRangeOffsetBasePos + i * 2));
}
for (var s = 0; s < segCount - 1; s++) {
final startCode = startCodes[s];
final endCode = endCodes[s];
final idDelta = idDeltas[s];
final idRangeOffset = idRangeOffsets[s];
final idRangeOffsetAddress = idRangeOffsetBasePos + s * 2;
for (var c = startCode; c <= endCode; c++) {
var glyphIndex;
if (idRangeOffset == 0) {
glyphIndex = (idDelta + c) % 65536;
} else {
final glyphIndexAddress = idRangeOffset + 2 * (c - startCode) + idRangeOffsetAddress;
glyphIndex = bytes.getUint16(glyphIndexAddress);
}
charToGlyphIndexMap[c] = glyphIndex;
}
}
}
void _parseCMapFormat6(int basePosition, int length) {
final firstCode = bytes.getUint16(basePosition + 2);
final entryCount = bytes.getUint16(basePosition + 4);
for (var i = 0; i < entryCount; i++) {
final charCode = firstCode + i;
final glyphIndex = bytes.getUint16(basePosition + i * 2 + 6);
if (glyphIndex > 0) {
charToGlyphIndexMap[charCode] = glyphIndex;
}
}
}
void _parseIndexes() {
final basePosition = _tableOffsets[_LOCA];
final numGlyphs = this.numGlyphs;
if (indexToLocFormat == 0) {
for (var i = 0; i < numGlyphs; i++) {
glyphOffsets.add(bytes.getUint16(basePosition + i * 2) * 2);
}
} else {
for (var i = 0; i < numGlyphs; i++) {
glyphOffsets.add(bytes.getUint32(basePosition + i * 4));
}
}
}
void _parseGlyf() {
final baseOffset = _tableOffsets[_GLYF];
final unitsPerEm = this.unitsPerEm;
int glyphIndex = 0;
for (var offset in glyphOffsets) {
final xMin = bytes.getInt16(baseOffset + offset + 2); // 2
final yMin = bytes.getInt16(baseOffset + offset + 4); // 4
final xMax = bytes.getInt16(baseOffset + offset + 6); // 6
final yMax = bytes.getInt16(baseOffset + offset + 8); // 8
glyphInfoMap[glyphIndex] = new PDFRect(
xMin.toDouble() / unitsPerEm,
yMin.toDouble() / unitsPerEm,
xMax.toDouble() / unitsPerEm,
yMax.toDouble() / unitsPerEm);
glyphIndex++;
}
}
}
... ...
... ... @@ -23,19 +23,20 @@ class PDFTTFFont extends PDFFont {
PDFFontDescriptor descriptor;
PDFArrayObject widthsObject;
final widths = new List<String>();
TtfFont _font;
final TTFParser font;
int _charMin;
int _charMax;
/// Constructs a PDFTTFFont
PDFTTFFont(PDFDocument pdfDocument, Uint8List bytes)
: super(pdfDocument, subtype: "/TrueType") {
_font = new TtfParser().parse(bytes);
baseFont = "/" + _font.name.fontName.replaceAll(" ", "");
PDFTTFFont(PDFDocument pdfDocument, ByteData bytes)
: font = new TTFParser(bytes),
super(pdfDocument, subtype: "/TrueType") {
baseFont = "/" + font.fontName.replaceAll(" ", "");
PDFObjectStream file = new PDFObjectStream(pdfDocument, isBinary: true);
file.buf.putBytes(bytes);
file.params["/Length1"] = PDFStream.intNum(bytes.length);
final data = bytes.buffer.asUint8List();
file.buf.putBytes(data);
file.params["/Length1"] = PDFStream.intNum(data.length);
_charMin = 32;
_charMax = 255;
... ... @@ -45,35 +46,30 @@ class PDFTTFFont extends PDFFont {
}
unicodeCMap = new PDFObject(pdfDocument);
descriptor = new PDFFontDescriptor(this, file, _font);
descriptor = new PDFFontDescriptor(this, file);
widthsObject = new PDFArrayObject(pdfDocument, widths);
}
@override
double glyphAdvance(int charCode) {
var g = _font.cmap.charToGlyphIndexMap[charCode];
var g = font.charToGlyphIndexMap[charCode];
if (g == null) {
return super.glyphAdvance(charCode);
}
return _font.hmtx.metrics[g].advanceWidth / _font.head.unitsPerEm;
return (font.advanceWidth[g]) ?? super.glyphAdvance(charCode);
}
@override
PDFRect glyphBounds(int charCode) {
var g = _font.cmap.charToGlyphIndexMap[charCode];
var g = font.charToGlyphIndexMap[charCode];
if (g == null) {
return super.glyphBounds(charCode);
}
var info = _font.glyf.glyphInfoMap[g];
return new PDFRect(
info.xMin.toDouble() / _font.head.unitsPerEm,
info.yMin.toDouble() / _font.head.unitsPerEm,
(info.xMax - info.xMin).toDouble() / _font.head.unitsPerEm,
(info.yMax - info.yMin).toDouble() / _font.head.unitsPerEm);
return font.glyphInfoMap[g] ?? super.glyphBounds(charCode);
}
@override
... ...
... ... @@ -2,14 +2,13 @@ name: pdf
author: David PHAM-VAN <dev.nfet.net@gmail.com>
description: A pdf producer for Dart. It can create pdf files for both web or flutter.
homepage: https://github.com/davbfr/dart_pdf
version: 1.0.2
version: 1.0.3
environment:
sdk: ">=1.8.0 <3.0.0"
dependencies:
meta: "^1.1.5"
ttf_parser: "^1.0.0"
vector_math: "^2.0.0"
dev_dependencies:
... ...
import 'dart:io';
import 'dart:math';
import 'dart:typed_data';
import 'package:pdf/pdf.dart';
import 'package:test/test.dart';
... ... @@ -27,8 +28,11 @@ void main() {
g.restoreContext();
var font1 = new PDFFont(pdf);
var font2 =
new PDFTTFFont(pdf, new File("../assets/Nunito-Regular.ttf").readAsBytesSync());
var font2 = new PDFTTFFont(
pdf,
(new File("../assets/Nunito-Regular.ttf").readAsBytesSync() as Uint8List)
.buffer
.asByteData());
var s = "Hello World!";
var r = font2.stringBounds(s);
const FS = 20.0;
... ...
import 'dart:io';
import 'dart:typed_data';
import 'package:pdf/pdf.dart';
import 'package:test/test.dart';
void main() {
test('Pdf', () {
var pdf = new PDFDocument(deflate: false);
var i = pdf.info;
i.author = "David PHAM-VAN";
i.creator = i.author;
i.title = "My Title";
i.subject = "My Subject";
var page = new PDFPage(pdf, pageFormat: const PDFPageFormat(500.0, 300.0));
var g = page.getGraphics();
var ttf = new PDFTTFFont(
pdf,
(new File("../assets/Nunito-Regular.ttf").readAsBytesSync() as Uint8List)
.buffer
.asByteData());
var s = "Hello World!";
var r = ttf.stringBounds(s);
print(r);
const FS = 20.0;
g.setColor(new PDFColor(0.0, 1.0, 1.0));
g.drawRect(50.0 + r.x * FS, 30.0 + r.y * FS, r.w * FS, r.h * FS);
g.fillPath();
g.setColor(new PDFColor(0.3, 0.3, 0.3));
g.drawString(ttf, FS, s, 50.0, 30.0);
var file = new File('file.pdf');
file.writeAsBytesSync(pdf.save());
});
}
... ...