svg sizing
This commit is contained in:
parent
4a6622de49
commit
c9fb94f326
9 changed files with 164 additions and 90 deletions
|
@ -7,6 +7,7 @@ import 'package:aves/model/metadata_db.dart';
|
||||||
import 'package:aves/services/image_file_service.dart';
|
import 'package:aves/services/image_file_service.dart';
|
||||||
import 'package:aves/services/metadata_service.dart';
|
import 'package:aves/services/metadata_service.dart';
|
||||||
import 'package:aves/services/service_policy.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/change_notifier.dart';
|
||||||
import 'package:aves/utils/math_utils.dart';
|
import 'package:aves/utils/math_utils.dart';
|
||||||
import 'package:aves/utils/time_utils.dart';
|
import 'package:aves/utils/time_utils.dart';
|
||||||
|
@ -24,8 +25,6 @@ class ImageEntry {
|
||||||
String _path, _directory, _filename, _extension;
|
String _path, _directory, _filename, _extension;
|
||||||
int contentId;
|
int contentId;
|
||||||
final String sourceMimeType;
|
final String sourceMimeType;
|
||||||
|
|
||||||
// TODO TLAD use SVG viewport as width/height
|
|
||||||
int width;
|
int width;
|
||||||
int height;
|
int height;
|
||||||
int sourceRotationDegrees;
|
int sourceRotationDegrees;
|
||||||
|
@ -236,10 +235,24 @@ class ImageEntry {
|
||||||
// but it would take space and time, so a basic workaround will do.
|
// but it would take space and time, so a basic workaround will do.
|
||||||
bool get isPortrait => rotationDegrees % 180 == 90 && (catalogMetadata?.rotationDegrees == null || width > height);
|
bool get isPortrait => rotationDegrees % 180 == 90 && (catalogMetadata?.rotationDegrees == null || width > height);
|
||||||
|
|
||||||
|
static const ratioSeparator = '\u2236';
|
||||||
|
static const resolutionSeparator = ' \u00D7 ';
|
||||||
|
|
||||||
String get resolutionText {
|
String get resolutionText {
|
||||||
final w = width ?? '?';
|
final w = width ?? '?';
|
||||||
final h = height ?? '?';
|
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 {
|
double get displayAspectRatio {
|
||||||
|
@ -319,7 +332,7 @@ class ImageEntry {
|
||||||
String _bestTitle;
|
String _bestTitle;
|
||||||
|
|
||||||
String get bestTitle {
|
String get bestTitle {
|
||||||
_bestTitle ??= (isCatalogued && _catalogMetadata.xmpTitleDescription.isNotEmpty) ? _catalogMetadata.xmpTitleDescription : sourceTitle;
|
_bestTitle ??= (isCatalogued && _catalogMetadata.xmpTitleDescription?.isNotEmpty == true) ? _catalogMetadata.xmpTitleDescription : sourceTitle;
|
||||||
return _bestTitle;
|
return _bestTitle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -350,7 +363,20 @@ class ImageEntry {
|
||||||
|
|
||||||
Future<void> catalog({bool background = false}) async {
|
Future<void> catalog({bool background = false}) async {
|
||||||
if (isCatalogued) return;
|
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;
|
AddressDetails get addressDetails => _addressDetails;
|
||||||
|
@ -447,6 +473,12 @@ class ImageEntry {
|
||||||
this.sourceTitle = sourceTitle;
|
this.sourceTitle = sourceTitle;
|
||||||
_bestTitle = null;
|
_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'];
|
final dateModifiedSecs = newFields['dateModifiedSecs'];
|
||||||
if (dateModifiedSecs is int) this.dateModifiedSecs = dateModifiedSecs;
|
if (dateModifiedSecs is int) this.dateModifiedSecs = dateModifiedSecs;
|
||||||
final rotationDegrees = newFields['rotationDegrees'];
|
final rotationDegrees = newFields['rotationDegrees'];
|
||||||
|
|
|
@ -46,10 +46,10 @@ class CatalogMetadata {
|
||||||
this.contentId,
|
this.contentId,
|
||||||
this.mimeType,
|
this.mimeType,
|
||||||
this.dateMillis,
|
this.dateMillis,
|
||||||
this.isAnimated,
|
this.isAnimated = false,
|
||||||
this.isFlipped,
|
this.isFlipped = false,
|
||||||
this.isGeotiff,
|
this.isGeotiff = false,
|
||||||
this.is360,
|
this.is360 = false,
|
||||||
this.rotationDegrees,
|
this.rotationDegrees,
|
||||||
this.xmpSubjects,
|
this.xmpSubjects,
|
||||||
this.xmpTitleDescription,
|
this.xmpTitleDescription,
|
||||||
|
|
|
@ -23,7 +23,7 @@ mixin TagMixin on SourceBase {
|
||||||
|
|
||||||
Future<void> catalogEntries() async {
|
Future<void> catalogEntries() async {
|
||||||
// final stopwatch = Stopwatch()..start();
|
// 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;
|
if (todo.isEmpty) return;
|
||||||
|
|
||||||
var progressDone = 0;
|
var progressDone = 0;
|
||||||
|
|
86
lib/services/svg_metadata_service.dart
Normal file
86
lib/services/svg_metadata_service.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -70,7 +70,7 @@ class _MagnifierState extends State<Magnifier> {
|
||||||
MagnifierScaleStateController _scaleStateController;
|
MagnifierScaleStateController _scaleStateController;
|
||||||
|
|
||||||
void _setChildSize(Size childSize) {
|
void _setChildSize(Size childSize) {
|
||||||
_childSize = childSize;
|
_childSize = childSize.isEmpty ? null : childSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
|
@ -160,9 +160,11 @@ class _ImageViewState extends State<ImageView> {
|
||||||
colorFilter: colorFilter,
|
colorFilter: colorFilter,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
childSize: entry.displaySize,
|
||||||
controller: _magnifierController,
|
controller: _magnifierController,
|
||||||
minScale: minScale,
|
minScale: minScale,
|
||||||
initialScale: initialScale,
|
initialScale: initialScale,
|
||||||
|
scaleStateCycle: _vectorScaleStateCycle,
|
||||||
onTap: (c, d, s, childPosition) => onTap?.call(childPosition),
|
onTap: (c, d, s, childPosition) => onTap?.call(childPosition),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -190,6 +192,15 @@ class _ImageViewState extends State<ImageView> {
|
||||||
_viewStateNotifier.value = viewState;
|
_viewStateNotifier.value = viewState;
|
||||||
ViewStateNotification(entry.uri, viewState).dispatch(context);
|
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 {
|
class ViewState {
|
||||||
|
|
|
@ -3,6 +3,7 @@ import 'dart:collection';
|
||||||
import 'package:aves/model/image_entry.dart';
|
import 'package:aves/model/image_entry.dart';
|
||||||
import 'package:aves/ref/brand_colors.dart';
|
import 'package:aves/ref/brand_colors.dart';
|
||||||
import 'package:aves/services/metadata_service.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/durations.dart';
|
||||||
import 'package:aves/theme/icons.dart';
|
import 'package:aves/theme/icons.dart';
|
||||||
import 'package:aves/utils/color_utils.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/common/identity/aves_expansion_tile.dart';
|
||||||
import 'package:aves/widgets/fullscreen/info/common.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/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/info/metadata/xmp_tile.dart';
|
||||||
|
import 'package:aves/widgets/fullscreen/source_viewer_page.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
@ -175,7 +176,7 @@ class _MetadataSectionSliverState extends State<MetadataSectionSliver> with Auto
|
||||||
child: InfoRowGroup(
|
child: InfoRowGroup(
|
||||||
dir.tags,
|
dir.tags,
|
||||||
maxValueLength: Constants.infoGroupMaxValueLength,
|
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 (entry == null) return;
|
||||||
if (_loadedMetadataUri.value == entry.uri) return;
|
if (_loadedMetadataUri.value == entry.uri) return;
|
||||||
if (isVisible) {
|
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) {
|
final directories = rawMetadata.entries.map((dirKV) {
|
||||||
var directoryName = dirKV.key as String ?? '';
|
var directoryName = dirKV.key as String ?? '';
|
||||||
|
|
||||||
|
@ -230,6 +231,25 @@ class _MetadataSectionSliverState extends State<MetadataSectionSliver> with Auto
|
||||||
_expandedDirectoryNotifier.value = null;
|
_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
|
@override
|
||||||
bool get wantKeepAlive => true;
|
bool get wantKeepAlive => true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -278,7 +278,7 @@ class _DateRow extends StatelessWidget {
|
||||||
DecoratedIcon(AIcons.date, shadows: [Constants.embossShadow], size: _iconSize),
|
DecoratedIcon(AIcons.date, shadows: [Constants.embossShadow], size: _iconSize),
|
||||||
SizedBox(width: _iconPadding),
|
SizedBox(width: _iconPadding),
|
||||||
Expanded(flex: 3, child: Text(dateText, strutStyle: Constants.overflowStrutStyle)),
|
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)),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue