svg sizing

This commit is contained in:
Thibault Deckers 2020-12-18 11:44:07 +09:00
parent 4a6622de49
commit c9fb94f326
9 changed files with 164 additions and 90 deletions

View file

@ -7,6 +7,7 @@ import 'package:aves/model/metadata_db.dart';
import 'package:aves/services/image_file_service.dart';
import 'package:aves/services/metadata_service.dart';
import 'package:aves/services/service_policy.dart';
import 'package:aves/services/svg_metadata_service.dart';
import 'package:aves/utils/change_notifier.dart';
import 'package:aves/utils/math_utils.dart';
import 'package:aves/utils/time_utils.dart';
@ -24,8 +25,6 @@ class ImageEntry {
String _path, _directory, _filename, _extension;
int contentId;
final String sourceMimeType;
// TODO TLAD use SVG viewport as width/height
int width;
int height;
int sourceRotationDegrees;
@ -236,10 +235,24 @@ class ImageEntry {
// but it would take space and time, so a basic workaround will do.
bool get isPortrait => rotationDegrees % 180 == 90 && (catalogMetadata?.rotationDegrees == null || width > height);
static const ratioSeparator = '\u2236';
static const resolutionSeparator = ' \u00D7 ';
String get resolutionText {
final w = width ?? '?';
final h = height ?? '?';
return isPortrait ? '$h × $w' : '$w × $h';
return isPortrait ? '$h$resolutionSeparator$w' : '$w$resolutionSeparator$h';
}
String get aspectRatioText {
if (width != null && height != null && width > 0 && height > 0) {
final gcd = width.gcd(height);
final w = width ~/ gcd;
final h = height ~/ gcd;
return isPortrait ? '$h$ratioSeparator$w' : '$w$ratioSeparator$h';
} else {
return '?$ratioSeparator?';
}
}
double get displayAspectRatio {
@ -319,7 +332,7 @@ class ImageEntry {
String _bestTitle;
String get bestTitle {
_bestTitle ??= (isCatalogued && _catalogMetadata.xmpTitleDescription.isNotEmpty) ? _catalogMetadata.xmpTitleDescription : sourceTitle;
_bestTitle ??= (isCatalogued && _catalogMetadata.xmpTitleDescription?.isNotEmpty == true) ? _catalogMetadata.xmpTitleDescription : sourceTitle;
return _bestTitle;
}
@ -350,7 +363,20 @@ class ImageEntry {
Future<void> catalog({bool background = false}) async {
if (isCatalogued) return;
catalogMetadata = await MetadataService.getCatalogMetadata(this, background: background);
if (isSvg) {
// vector image sizing is not essential, so we should not spend time for it during loading
// but it is useful anyway (for aspect ratios etc.) so we size them during cataloguing
final size = await SvgMetadataService.getSize(this);
if (size != null) {
await _applyNewFields({
'width': size.width.round(),
'height': size.height.round(),
});
}
catalogMetadata = CatalogMetadata(contentId: contentId);
} else {
catalogMetadata = await MetadataService.getCatalogMetadata(this, background: background);
}
}
AddressDetails get addressDetails => _addressDetails;
@ -447,6 +473,12 @@ class ImageEntry {
this.sourceTitle = sourceTitle;
_bestTitle = null;
}
final width = newFields['width'];
if (width is int) this.width = width;
final height = newFields['height'];
if (height is int) this.height = height;
final dateModifiedSecs = newFields['dateModifiedSecs'];
if (dateModifiedSecs is int) this.dateModifiedSecs = dateModifiedSecs;
final rotationDegrees = newFields['rotationDegrees'];

View file

@ -46,10 +46,10 @@ class CatalogMetadata {
this.contentId,
this.mimeType,
this.dateMillis,
this.isAnimated,
this.isFlipped,
this.isGeotiff,
this.is360,
this.isAnimated = false,
this.isFlipped = false,
this.isGeotiff = false,
this.is360 = false,
this.rotationDegrees,
this.xmpSubjects,
this.xmpTitleDescription,

View file

@ -23,7 +23,7 @@ mixin TagMixin on SourceBase {
Future<void> catalogEntries() async {
// final stopwatch = Stopwatch()..start();
final todo = rawEntries.where((entry) => !entry.isCatalogued && !entry.isSvg).toList();
final todo = rawEntries.where((entry) => !entry.isCatalogued).toList();
if (todo.isEmpty) return;
var progressDone = 0;

View file

@ -0,0 +1,86 @@
import 'dart:convert';
import 'package:aves/model/image_entry.dart';
import 'package:aves/services/image_file_service.dart';
import 'package:aves/utils/string_utils.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:xml/xml.dart';
class SvgMetadataService {
static const docDirectory = 'Document';
static const metadataDirectory = 'Metadata';
static const _attributes = ['x', 'y', 'width', 'height', 'preserveAspectRatio', 'viewBox'];
static const _textElements = ['title', 'desc'];
static const _metadataElement = 'metadata';
static Future<Size> getSize(ImageEntry entry) async {
try {
final data = await ImageFileService.getImage(entry.uri, entry.mimeType, 0, false);
final document = XmlDocument.parse(utf8.decode(data));
final root = document.rootElement;
String getAttribute(String attributeName) => root.attributes.firstWhere((a) => a.name.qualified == attributeName, orElse: () => null)?.value;
double tryParseWithoutUnit(String s) => s == null ? null : double.tryParse(s.replaceAll(RegExp(r'[a-z%]'), ''));
final width = tryParseWithoutUnit(getAttribute('width'));
final height = tryParseWithoutUnit(getAttribute('height'));
if (width != null && height != null) {
return Size(width, height);
}
final viewBox = getAttribute('viewBox');
if (viewBox != null) {
final parts = viewBox.split(RegExp(r'[\s,]+'));
if (parts.length == 4) {
final vbWidth = tryParseWithoutUnit(parts[2]);
final vbHeight = tryParseWithoutUnit(parts[3]);
if (vbWidth > 0 && vbHeight > 0) {
return Size(vbWidth, vbHeight);
}
}
}
} catch (exception, stack) {
debugPrint('failed to parse XML from SVG with exception=$exception\n$stack');
}
return null;
}
static Future<Map<String, Map<String, String>>> getAllMetadata(ImageEntry entry) async {
String formatKey(String key) {
switch (key) {
case 'desc':
return 'Description';
default:
return key.toSentenceCase();
}
}
try {
final data = await ImageFileService.getImage(entry.uri, entry.mimeType, 0, false);
final document = XmlDocument.parse(utf8.decode(data));
final root = document.rootElement;
final docDir = Map.fromEntries([
...root.attributes.where((a) => _attributes.contains(a.name.qualified)).map((a) => MapEntry(formatKey(a.name.qualified), a.value)),
..._textElements.map((name) => MapEntry(formatKey(name), root.getElement(name)?.text)).where((kv) => kv.value != null),
]);
final metadata = root.getElement(_metadataElement);
final metadataDir = Map.fromEntries([
if (metadata != null) MapEntry('Metadata', metadata.toXmlString(pretty: true)),
]);
return {
if (docDir.isNotEmpty) docDirectory: docDir,
if (metadataDir.isNotEmpty) metadataDirectory: metadataDir,
};
} catch (exception, stack) {
debugPrint('failed to parse XML from SVG with exception=$exception\n$stack');
return null;
}
}
}

View file

@ -70,7 +70,7 @@ class _MagnifierState extends State<Magnifier> {
MagnifierScaleStateController _scaleStateController;
void _setChildSize(Size childSize) {
_childSize = childSize;
_childSize = childSize.isEmpty ? null : childSize;
}
@override

View file

@ -160,9 +160,11 @@ class _ImageViewState extends State<ImageView> {
colorFilter: colorFilter,
),
),
childSize: entry.displaySize,
controller: _magnifierController,
minScale: minScale,
initialScale: initialScale,
scaleStateCycle: _vectorScaleStateCycle,
onTap: (c, d, s, childPosition) => onTap?.call(childPosition),
);
}
@ -190,6 +192,15 @@ class _ImageViewState extends State<ImageView> {
_viewStateNotifier.value = viewState;
ViewStateNotification(entry.uri, viewState).dispatch(context);
}
static ScaleState _vectorScaleStateCycle(ScaleState actual) {
switch (actual) {
case ScaleState.initial:
return ScaleState.covering;
default:
return ScaleState.initial;
}
}
}
class ViewState {

View file

@ -3,6 +3,7 @@ import 'dart:collection';
import 'package:aves/model/image_entry.dart';
import 'package:aves/ref/brand_colors.dart';
import 'package:aves/services/metadata_service.dart';
import 'package:aves/services/svg_metadata_service.dart';
import 'package:aves/theme/durations.dart';
import 'package:aves/theme/icons.dart';
import 'package:aves/utils/color_utils.dart';
@ -10,8 +11,8 @@ import 'package:aves/utils/constants.dart';
import 'package:aves/widgets/common/identity/aves_expansion_tile.dart';
import 'package:aves/widgets/fullscreen/info/common.dart';
import 'package:aves/widgets/fullscreen/info/metadata/metadata_thumbnail.dart';
import 'package:aves/widgets/fullscreen/info/metadata/svg_tile.dart';
import 'package:aves/widgets/fullscreen/info/metadata/xmp_tile.dart';
import 'package:aves/widgets/fullscreen/source_viewer_page.dart';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
@ -175,7 +176,7 @@ class _MetadataSectionSliverState extends State<MetadataSectionSliver> with Auto
child: InfoRowGroup(
dir.tags,
maxValueLength: Constants.infoGroupMaxValueLength,
linkHandlers: dirName == SvgMetadata.metadataDirectory ? SvgMetadata.getLinkHandlers(dir.tags) : null,
linkHandlers: dirName == SvgMetadataService.metadataDirectory ? getSvgLinkHandlers(dir.tags) : null,
),
),
],
@ -192,7 +193,7 @@ class _MetadataSectionSliverState extends State<MetadataSectionSliver> with Auto
if (entry == null) return;
if (_loadedMetadataUri.value == entry.uri) return;
if (isVisible) {
final rawMetadata = await (entry.isSvg ? SvgMetadata.getAllMetadata(entry) : MetadataService.getAllMetadata(entry)) ?? {};
final rawMetadata = await (entry.isSvg ? SvgMetadataService.getAllMetadata(entry) : MetadataService.getAllMetadata(entry)) ?? {};
final directories = rawMetadata.entries.map((dirKV) {
var directoryName = dirKV.key as String ?? '';
@ -230,6 +231,25 @@ class _MetadataSectionSliverState extends State<MetadataSectionSliver> with Auto
_expandedDirectoryNotifier.value = null;
}
static Map<String, InfoLinkHandler> getSvgLinkHandlers(SplayTreeMap<String, String> tags) {
return {
'Metadata': InfoLinkHandler(
linkText: 'View XML',
onTap: (context) {
Navigator.push(
context,
MaterialPageRoute(
settings: RouteSettings(name: SourceViewerPage.routeName),
builder: (context) => SourceViewerPage(
loader: () => SynchronousFuture(tags['Metadata']),
),
),
);
},
),
};
}
@override
bool get wantKeepAlive => true;
}

View file

@ -1,75 +0,0 @@
import 'dart:collection';
import 'dart:convert';
import 'package:aves/model/image_entry.dart';
import 'package:aves/services/image_file_service.dart';
import 'package:aves/utils/string_utils.dart';
import 'package:aves/widgets/fullscreen/info/common.dart';
import 'package:aves/widgets/fullscreen/source_viewer_page.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:xml/xml.dart';
class SvgMetadata {
static const docDirectory = 'Document';
static const metadataDirectory = 'Metadata';
static const _attributes = ['x', 'y', 'width', 'height', 'preserveAspectRatio', 'viewBox'];
static const _textElements = ['title', 'desc'];
static const _metadataElement = 'metadata';
static Future<Map<String, Map<String, String>>> getAllMetadata(ImageEntry entry) async {
try {
final data = await ImageFileService.getImage(entry.uri, entry.mimeType, 0, false);
final document = XmlDocument.parse(utf8.decode(data));
final root = document.rootElement;
final docDir = Map.fromEntries([
...root.attributes.where((a) => _attributes.contains(a.name.qualified)).map((a) => MapEntry(_formatKey(a.name.qualified), a.value)),
..._textElements.map((name) => MapEntry(_formatKey(name), root.getElement(name)?.text)).where((kv) => kv.value != null),
]);
final metadata = root.getElement(_metadataElement);
final metadataDir = Map.fromEntries([
if (metadata != null) MapEntry('Metadata', metadata.toXmlString(pretty: true)),
]);
return {
if (docDir.isNotEmpty) docDirectory: docDir,
if (metadataDir.isNotEmpty) metadataDirectory: metadataDir,
};
} catch (exception, stack) {
debugPrint('failed to parse XML from SVG with exception=$exception\n$stack');
return null;
}
}
static Map<String, InfoLinkHandler> getLinkHandlers(SplayTreeMap<String, String> tags) {
return {
'Metadata': InfoLinkHandler(
linkText: 'View XML',
onTap: (context) {
Navigator.push(
context,
MaterialPageRoute(
settings: RouteSettings(name: SourceViewerPage.routeName),
builder: (context) => SourceViewerPage(
loader: () => SynchronousFuture(tags['Metadata']),
),
),
);
},
),
};
}
static String _formatKey(String key) {
switch (key) {
case 'desc':
return 'Description';
default:
return key.toSentenceCase();
}
}
}

View file

@ -278,7 +278,7 @@ class _DateRow extends StatelessWidget {
DecoratedIcon(AIcons.date, shadows: [Constants.embossShadow], size: _iconSize),
SizedBox(width: _iconPadding),
Expanded(flex: 3, child: Text(dateText, strutStyle: Constants.overflowStrutStyle)),
if (!entry.isSvg) Expanded(flex: 2, child: Text(entry.resolutionText, strutStyle: Constants.overflowStrutStyle)),
Expanded(flex: 2, child: Text(entry.isSvg ? entry.aspectRatioText : entry.resolutionText, strutStyle: Constants.overflowStrutStyle)),
],
);
}