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/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'];
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
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;
|
||||
|
||||
void _setChildSize(Size childSize) {
|
||||
_childSize = childSize;
|
||||
_childSize = childSize.isEmpty ? null : childSize;
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
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)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue