lots of ios file related updates, now use URLs internally. icons added.

This commit is contained in:
essej
2022-04-13 13:48:09 -04:00
parent e2178da076
commit dd89d80959
18 changed files with 169 additions and 69 deletions

View File

@ -13,4 +13,12 @@ void getSafeAreaInsets(void * component, float & top, float & bottom, float & le
void disableAppNap();
#endif
#if JUCE_IOS
bool urlBookmarkToBinaryData(void * bookmark, const void * & retdata, size_t & retsize);
void *binaryDataToUrlBookmark(const void * data, size_t size);
#endif

View File

@ -59,4 +59,23 @@ void getSafeAreaInsets(void * component, float & top, float & bottom, float & le
}
}
bool urlBookmarkToBinaryData(void * bookmark, const void * & retdata, size_t & retsize)
{
NSData * data = (NSData*) bookmark;
if (data && [data isKindOfClass:NSData.class]) {
retdata = [data bytes];
retsize = [data length];
return true;
}
return false;
}
void * binaryDataToUrlBookmark(const void * data, size_t size)
{
NSData * nsdata = [[NSData alloc] initWithBytes:data length:size];
return nsdata;
}
#endif

View File

@ -71,10 +71,10 @@ public:
return &m_readbuf;
return nullptr;
}
bool openAudioFile(File file) override
bool openAudioFile(const URL & url) override
{
m_silenceoutputted = 0;
AudioFormatReader* reader = m_manager->createReaderFor(file);
AudioFormatReader* reader = m_manager->createReaderFor(url.getLocalFile());
if (reader)
{
ScopedLock locker(m_mutex);

View File

@ -35,7 +35,7 @@ public:
{
};
[[nodiscard]] virtual bool openAudioFile(File file)=0;
[[nodiscard]] virtual bool openAudioFile(const URL & url)=0;
virtual void close()=0;
virtual int readNextBlock(AudioBuffer<float>& abuf, int smps, int numchans)=0;

View File

@ -158,7 +158,7 @@ void StretchAudioSource::setAudioBufferAsInputSource(AudioBuffer<float>* buf, in
m_inputfile->setAudioBuffer(buf, sr, len);
m_seekpos = 0.0;
m_audiobuffer_is_source = true;
m_curfile = File();
m_curfile = URL();
if (m_playrange.isEmpty())
setPlayRange({ 0.0,1.0 });
++m_param_change_count;
@ -505,12 +505,12 @@ bool StretchAudioSource::isLooping() const
return false;
}
String StretchAudioSource::setAudioFile(File file)
String StretchAudioSource::setAudioFile(const URL & url)
{
ScopedLock locker(m_cs);
if (m_inputfile->openAudioFile(file))
if (m_inputfile->openAudioFile(url))
{
m_curfile = file;
m_curfile = url;
m_firstbuffer = true;
m_audiobuffer_is_source = false;
return String();
@ -518,7 +518,7 @@ String StretchAudioSource::setAudioFile(File file)
return "Could not open file";
}
File StretchAudioSource::getAudioFile()
URL StretchAudioSource::getAudioFile()
{
return m_curfile;
}

View File

@ -46,8 +46,8 @@ public:
bool isLooping() const override;
String setAudioFile(File file);
File getAudioFile();
String setAudioFile(const URL & file);
URL getAudioFile();
AudioBuffer<float>* getSourceAudioBuffer();
@ -148,7 +148,7 @@ private:
bool m_stream_end_reached = false;
int64_t m_output_silence_counter = 0;
File m_curfile;
URL m_curfile;
bool m_audiobuffer_is_source = false;
int64_t m_maxloops = 0;
std::unique_ptr<WDL_Resampler> m_resampler;

View File

@ -160,11 +160,20 @@ PaulstretchpluginAudioProcessorEditor::PaulstretchpluginAudioProcessorEditor(Pau
}
}
m_parcomps[cpi_num_inchans]->getSlider()->setSliderStyle(Slider::SliderStyle::IncDecButtons);
m_parcomps[cpi_num_inchans]->getSlider()->setTextBoxStyle(Slider::TextEntryBoxPosition::TextBoxLeft, false, 30, 34);
m_parcomps[cpi_num_outchans]->getSlider()->setSliderStyle(Slider::SliderStyle::IncDecButtons);
m_parcomps[cpi_num_outchans]->getSlider()->setTextBoxStyle(Slider::TextEntryBoxPosition::TextBoxLeft, false, 30, 34);
#if JUCE_IOS
// just don't include chan counts on ios for now
removeChildComponent(m_parcomps[cpi_num_inchans].get());
removeChildComponent(m_parcomps[cpi_num_outchans].get());
#endif
m_groupviewport = std::make_unique<Viewport>();
m_groupcontainer = std::make_unique<Component>();
m_groupviewport->setViewedComponent(m_groupcontainer.get(), false);
@ -581,6 +590,7 @@ void PaulstretchpluginAudioProcessorEditor::resized()
togglesbox.items.add(FlexItem(toggleminw, togglerowheight, *m_parcomps[cpi_capture_trigger]).withMargin(margin).withFlex(1).withMaxWidth(200));
togglesbox.items.add(FlexItem(toggleminw, togglerowheight, *m_parcomps[cpi_passthrough]).withMargin(margin).withFlex(1.5).withMaxWidth(200));
togglesbox.items.add(FlexItem(toggleminw, togglerowheight, *m_parcomps[cpi_pause_enabled]).withMargin(margin).withFlex(1).withMaxWidth(200));
togglesbox.items.add(FlexItem(toggleminw, togglerowheight, *m_parcomps[cpi_freeze]).withMargin(margin).withFlex(1).withMaxWidth(200));
togglesbox.items.add(FlexItem(toggleminw, togglerowheight, *m_parcomps[cpi_bypass_stretch]).withMargin(margin).withFlex(1).withMaxWidth(200));
@ -596,6 +606,8 @@ void PaulstretchpluginAudioProcessorEditor::resized()
volbox.alignContent = FlexBox::AlignContent::flexStart;
volbox.items.add(FlexItem(minitemw*0.75f, rowheight, *m_parcomps[cpi_main_volume]).withMargin(margin).withFlex(1));
#if !JUCE_IOS
FlexBox inoutbox;
int inoutminw = 170;
int inoutmaxw = 200;
@ -605,6 +617,7 @@ void PaulstretchpluginAudioProcessorEditor::resized()
inoutbox.items.add(FlexItem(inoutminw, rowheight, *m_parcomps[cpi_num_outchans]).withMargin(margin).withFlex(0.5).withMaxWidth(inoutmaxw));
volbox.items.add(FlexItem(2*inoutminw, rowheight, inoutbox).withMargin(margin).withFlex(1.5).withMaxWidth(2*inoutmaxw + 10));
#endif
volbox.performLayout(Rectangle<int>(0,0,w - 2*margin,400)); // test run to calculate actual used height
int volh = volbox.items.getLast().currentBounds.getBottom() + volbox.items.getLast().margin.bottom;
@ -888,8 +901,9 @@ void PaulstretchpluginAudioProcessorEditor::filesDropped(const StringArray & fil
{
if (files.size() > 0)
{
File f(files[0]);
processor.setAudioFile(f);
File file(files[0]);
URL url = URL(file);
processor.setAudioFile(url);
toFront(true);
}
}
@ -900,14 +914,9 @@ void PaulstretchpluginAudioProcessorEditor::urlOpened(const URL& url)
std::unique_ptr<InputStream> wi (url.createInputStream (false));
if (wi != nullptr)
{
File file = url.getLocalFile();
DBG("Attempting to load after input stream create: " << file.getFullPathName());
processor.setAudioFile(file);
} else {
File file = url.getLocalFile();
DBG("Attempting to load after no input stream create: " << file.getFullPathName());
processor.setAudioFile(file);
}
DBG("Attempting to load after input stream create: " << url.toString(false));
processor.setAudioFile(url);
}
toFront(true);
}
@ -1048,7 +1057,7 @@ void PaulstretchpluginAudioProcessorEditor::toggleFileBrowser()
DBG("Attempting to load from: " << file.getFullPathName());
//curropendir = file.getParentDirectory();
processor.setAudioFile(file);
processor.setAudioFile(url);
processor.m_propsfile->m_props_file->setValue("importfilefolder", file.getParentDirectory().getFullPathName());
}
}
@ -1223,7 +1232,7 @@ void WaveformComponent::paint(Graphics & g)
g.fillRect(normalizedToViewX<int>(m_rec_pos), m_topmargin, 1, getHeight() - m_topmargin);
}
g.setColour(Colours::aqua);
g.drawText(GetFileCallback().getFileName(), 2, m_topmargin + 2, getWidth(), 20, Justification::topLeft);
g.drawText(URL::removeEscapeChars(GetFileCallback().getFileName()), 2, m_topmargin + 2, getWidth(), 20, Justification::topLeft);
g.drawText(secondsToString2(thumblen), getWidth() - 200, m_topmargin + 2, 200, 20, Justification::topRight);
}
@ -2366,7 +2375,7 @@ void MyFileBrowserComponent::fileClicked(const File & file, const MouseEvent & e
void MyFileBrowserComponent::fileDoubleClicked(const File & file)
{
m_proc.setAudioFile(file);
m_proc.setAudioFile(URL(file));
m_proc.m_propsfile->m_props_file->setValue("importfilefolder", file.getParentDirectory().getFullPathName());
}

View File

@ -189,7 +189,7 @@ public:
std::function<double()> CursorPosCallback;
std::function<void(double)> SeekCallback;
std::function<void(Range<double>, int)> TimeSelectionChangedCallback;
std::function<File()> GetFileCallback;
std::function<URL()> GetFileCallback;
std::function<void(Range<double>)> ViewRangeChangedCallback;
void mouseDown(const MouseEvent& e) override;
void mouseUp(const MouseEvent& e) override;

View File

@ -20,6 +20,8 @@ www.gnu.org/licenses
#include <set>
#include <thread>
#import "CrossPlatformUtils.h"
#ifdef WIN32
#undef min
#undef max
@ -66,20 +68,21 @@ m_bufferingthread("pspluginprebufferthread"), m_is_stand_alone_offline(is_stand_
{
m_filechoose_callback = [this](const FileChooser& chooser)
{
File resu = chooser.getResult();
String pathname = resu.getFullPathName();
if (pathname.startsWith("/localhost"))
{
pathname = pathname.substring(10);
resu = File(pathname);
}
m_propsfile->m_props_file->setValue("importfilefolder", resu.getParentDirectory().getFullPathName());
String loaderr = setAudioFile(resu);
if (auto ed = dynamic_cast<PaulstretchpluginAudioProcessorEditor*>(getActiveEditor()); ed != nullptr)
{
ed->m_last_err = loaderr;
}
URL resu = chooser.getURLResult();
//String pathname = resu.getFullPathName();
//if (pathname.startsWith("/localhost"))
//{
// pathname = pathname.substring(10);
// resu = File(pathname);
//}
if (!resu.isEmpty()) {
m_propsfile->m_props_file->setValue("importfilefolder", resu.getLocalFile().getParentDirectory().getFullPathName());
String loaderr = setAudioFile(resu);
if (auto ed = dynamic_cast<PaulstretchpluginAudioProcessorEditor*>(getActiveEditor()); ed != nullptr)
{
ed->m_last_err = loaderr;
}
}
};
m_playposinfo.timeInSeconds = 0.0;
@ -268,9 +271,22 @@ ValueTree PaulstretchpluginAudioProcessor::getStateTree(bool ignoreoptions, bool
{
ValueTree paramtree("paulstretch3pluginstate");
storeToTreeProperties(paramtree, nullptr, getParameters(), { getBoolParameter(cpi_capture_trigger) });
if (m_current_file != File() && ignorefile == false)
if (m_current_file != URL() && ignorefile == false)
{
paramtree.setProperty("importedfile", m_current_file.getFullPathName(), nullptr);
paramtree.setProperty("importedfile", m_current_file.toString(false), nullptr);
#if JUCE_IOS
// store bookmark data if necessary
if (void * bookmark = getURLBookmark(m_current_file)) {
const void * data = nullptr;
size_t datasize = 0;
if (urlBookmarkToBinaryData(bookmark, data, datasize)) {
DBG("Audio file has bookmark, storing it in state, size: " << datasize);
paramtree.setProperty("importedfile_bookmark", var(data, datasize), nullptr);
} else {
DBG("Bookmark is not valid!");
}
}
#endif
}
auto specorder = m_stretch_source->getSpectrumProcessOrder();
paramtree.setProperty("numspectralstagesb", (int)specorder.size(), nullptr);
@ -342,10 +358,31 @@ void PaulstretchpluginAudioProcessor::setStateFromTree(ValueTree tree)
setPreBufferAmount(prebufamt);
if (m_load_file_with_state == true)
{
String fn = tree.getProperty("importedfile");
if (fn.isEmpty() == false)
String fn = tree.getProperty("importedfile");
if (fn.isNotEmpty())
{
setAudioFile(File(fn));
URL url(fn);
if (!url.isLocalFile()) {
// reconstruct just in case imported file string was not a URL
url = URL(File(fn));
}
#if JUCE_IOS
// check for bookmark
auto bptr = tree.getPropertyPointer("importedfile_bookmark");
if (bptr) {
if (auto * block = bptr->getBinaryData()) {
DBG("Has file bookmark");
void * bookmark = binaryDataToUrlBookmark(block->getData(), block->getSize());
setURLBookmark(url, bookmark);
}
}
else {
DBG("no url bookmark found");
}
#endif
setAudioFile(url);
}
}
m_state_dirty = true;
@ -546,7 +583,7 @@ void PaulstretchpluginAudioProcessor::saveCaptureBuffer()
jassert(sourcebuffer->getNumSamples() > 0);
writer->writeFromAudioSampleBuffer(*sourcebuffer, 0, sourcebuffer->getNumSamples());
m_current_file = outfile;
m_current_file = URL(outfile);
}
else
{
@ -982,7 +1019,7 @@ void PaulstretchpluginAudioProcessor::setRecordingEnabled(bool b)
if (b == true)
{
m_using_memory_buffer = true;
m_current_file = File();
m_current_file = URL();
int numchans = *m_inchansparam;
m_recbuffer.setSize(numchans, m_max_reclen*getSampleRateChecked()+4096,false,false,true);
m_recbuffer.clear();
@ -1008,33 +1045,44 @@ double PaulstretchpluginAudioProcessor::getRecordingPositionPercent()
return 1.0 / m_recbuffer.getNumSamples()*m_rec_pos;
}
String PaulstretchpluginAudioProcessor::setAudioFile(File f)
String PaulstretchpluginAudioProcessor::setAudioFile(const URL & url)
{
auto ai = unique_from_raw(m_afm->createReaderFor(f));
// this handles any permissions stuff (needed on ios)
std::unique_ptr<InputStream> wi (url.createInputStream (false));
File file = url.getLocalFile();
auto ai = unique_from_raw(m_afm->createReaderFor(file));
if (ai != nullptr)
{
if (ai->numChannels > 8)
{
return "Too many channels in file "+f.getFullPathName();
return "Too many channels in file "+ file.getFullPathName();
}
if (ai->bitsPerSample>32)
{
return "Too high bit depth in file " + f.getFullPathName();
return "Too high bit depth in file " + file.getFullPathName();
}
if (m_thumb)
m_thumb->setSource(new FileInputSource(f));
m_thumb->setSource(new FileInputSource(file));
ScopedLock locker(m_cs);
m_stretch_source->setAudioFile(f);
m_stretch_source->setAudioFile(url);
//Range<double> currange{ *getFloatParameter(cpi_soundstart),*getFloatParameter(cpi_soundend) };
//if (currange.contains(m_stretch_source->getInfilePositionPercent())==false)
m_stretch_source->seekPercent(*getFloatParameter(cpi_soundstart));
m_current_file = f;
m_current_file_date = m_current_file.getLastModificationTime();
m_current_file = url;
#if JUCE_IOS
if (void * bookmark = getURLBookmark(m_current_file)) {
DBG("Loaded audio file has bookmark");
}
#endif
m_current_file_date = file.getLastModificationTime();
m_using_memory_buffer = false;
setDirty();
return String();
}
return "Could not open file " + f.getFullPathName();
return "Could not open file " + file.getFullPathName();
}
Range<double> PaulstretchpluginAudioProcessor::getTimeSelection()
@ -1106,7 +1154,7 @@ pointer_sized_int PaulstretchpluginAudioProcessor::handleVstPluginCanDo(int32 in
{
String fn(CharPointer_UTF8((char*)value));
//std::cout << "host requested to set audio file " << fn << "\n";
auto err = setAudioFile(File(fn));
auto err = setAudioFile(URL(fn));
if (err.isEmpty()==false)
std::cout << err << "\n";
}
@ -1124,7 +1172,7 @@ pointer_sized_int PaulstretchpluginAudioProcessor::handleVstManufacturerSpecific
void PaulstretchpluginAudioProcessor::finishRecording(int lenrecording)
{
m_is_recording = false;
m_current_file = File();
m_current_file = URL();
m_stretch_source->setAudioBufferAsInputSource(&m_recbuffer, getSampleRateChecked(), lenrecording);
*getFloatParameter(cpi_soundstart) = 0.0f;
*getFloatParameter(cpi_soundend) = jlimit<double>(0.01, 1.0, 1.0 / lenrecording * m_rec_count);

View File

@ -200,8 +200,8 @@ public:
void setRecordingEnabled(bool b);
bool isRecordingEnabled() { return m_is_recording; }
double getRecordingPositionPercent();
String setAudioFile(File f);
File getAudioFile() { return m_current_file; }
String setAudioFile(const URL& url);
URL getAudioFile() { return m_current_file; }
Range<double> getTimeSelection();
SharedResourcePointer<AudioFormatManager> m_afm;
SharedResourcePointer<MyPropertiesFile> m_propsfile;
@ -261,7 +261,7 @@ private:
bool m_using_memory_buffer = true;
int m_cur_num_out_chans = 2;
CriticalSection m_cs;
File m_current_file;
URL m_current_file;
Time m_current_file_date;
bool m_is_recording = false;
TimeSliceThread m_bufferingthread;

View File

@ -1,71 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
inkscape:export-ydpi="9"
inkscape:export-xdpi="9"
inkscape:export-filename="/Users/jesse/src/sonobus/images/person.png"
inkscape:version="1.1 (c4e8f9e, 2021-05-24)"
sodipodi:docname="power.svg"
xml:space="preserve"
viewBox="0 0 512 512"
height="512px"
width="512px"
y="0px"
x="0px"
id="Layer_1"
version="1.1"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/"><defs
id="defs5984" /><sodipodi:namedview
inkscape:current-layer="Layer_1"
inkscape:window-maximized="0"
inkscape:window-y="468"
inkscape:window-x="1916"
inkscape:cy="198.26548"
inkscape:cx="241.87026"
inkscape:zoom="0.73386453"
showgrid="false"
id="namedview5982"
inkscape:window-height="677"
inkscape:window-width="1256"
inkscape:pageshadow="2"
inkscape:pageopacity="0"
guidetolerance="10"
gridtolerance="10"
objecttolerance="10"
borderopacity="1"
bordercolor="#666666"
pagecolor="#ffffff"
inkscape:document-rotation="0"
showguides="true"
inkscape:guide-bbox="true"
inkscape:pagecheckerboard="1" /><rect
y="2.7346985"
x="-2.7346985"
height="512"
width="512"
id="rect8965"
style="fill:#000000;fill-opacity:0;stroke-width:15" /><path
id="path5975"
d="M 256,256 Z" /><metadata
id="metadata5979"><rdf:RDF><rdf:Description
dc:language="en"
dc:format="image/svg+xml"
dc:date="2017-09-24"
dc:publisher="Iconscout"
dc:description="ios,person,outline"
dc:title="ios,person,outline"
about="https://iconscout.com/legal#licenses"><dc:creator><rdf:Bag><rdf:li>Benjamin J Sperry</rdf:li></rdf:Bag></dc:creator></rdf:Description><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><path
d="M 256,464 C 141.31,464 48,370.53 48,255.65 48,193.2 75.25,134.65 122.76,95.100001 A 22,22 0 1 1 150.93,128.9 C 113.48,160.1 92,206.3 92,255.65 92,346.27 165.57,420 256,420 346.43,420 420,346.27 420,255.65 A 164,164 0 0 0 360.17,129 22,22 0 1 1 388.17,95.080001 207.88,207.88 0 0 1 464,255.65 C 464,370.53 370.69,464 256,464 Z"
id="path4469"
style="fill:#b2b2b2;fill-opacity:1;stroke:none;stroke-opacity:1" /><path
d="m 256,188 c -12.15026,0 -22,-9.84974 -22,-22 V 70.000001 c 0,-12.150273 9.84974,-21.99999 22,-21.99999 12.15026,0 22,9.849717 22,21.99999 V 166 c 0,12.15026 -9.84974,22 -22,22 z"
id="path4471"
style="fill:#b2b2b2;fill-opacity:1;stroke:none;stroke-opacity:1"
sodipodi:nodetypes="sssssss" /></svg>

Before

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -1,72 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="Layer_1"
x="0px"
y="0px"
width="512px"
height="512px"
viewBox="0 0 512 512"
xml:space="preserve"
sodipodi:docname="power_sel.svg"
inkscape:version="1.0 (4035a4f, 2020-05-01)"
inkscape:export-filename="/Users/jesse/src/sonobus/images/person.png"
inkscape:export-xdpi="9"
inkscape:export-ydpi="9"><defs
id="defs5984" /><sodipodi:namedview
inkscape:guide-bbox="true"
showguides="true"
inkscape:document-rotation="0"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1000"
inkscape:window-height="677"
id="namedview5982"
showgrid="false"
inkscape:zoom="0.31557847"
inkscape:cx="215.97553"
inkscape:cy="339.55745"
inkscape:window-x="2365"
inkscape:window-y="560"
inkscape:window-maximized="0"
inkscape:current-layer="Layer_1" /><rect
style="fill:#000000;fill-opacity:0;stroke-width:15"
id="rect8965"
width="512"
height="512"
x="-2.7346985"
y="2.7346985" /><path
d="M 256,256 Z"
id="path5975" /><metadata
id="metadata5979"><rdf:RDF><rdf:Description
about="https://iconscout.com/legal#licenses"
dc:title="ios,person,outline"
dc:description="ios,person,outline"
dc:publisher="Iconscout"
dc:date="2017-09-24"
dc:format="image/svg+xml"
dc:language="en"><dc:creator><rdf:Bag><rdf:li>Benjamin J Sperry</rdf:li></rdf:Bag></dc:creator></rdf:Description><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><g
style="stroke:none;stroke-opacity:1;fill:#7dc5f8;fill-opacity:1"
transform="translate(-98.9038,78.548099)"
id="g5080"><path
style="stroke:none;stroke-opacity:1;fill:#7dc5f8;fill-opacity:1"
id="path4469"
d="m 354.9038,385.4519 c -114.69,0 -208,-93.47 -208,-208.35 0,-62.45 27.25,-120.999998 74.76,-160.549998 a 22,22 0 1 1 28.17,33.8 c -37.45,31.2 -58.93,77.399998 -58.93,126.749998 0,90.62 73.57,164.35 164,164.35 90.43,0 164,-73.73 164,-164.35 a 164,164 0 0 0 -59.83,-126.649998 22,22 0 1 1 28,-33.92 207.88,207.88 0 0 1 75.83,160.569998 c 0,114.88 -93.31,208.35 -208,208.35 z" /><path
style="stroke:none;stroke-opacity:1;fill:#7dc5f8;fill-opacity:1"
id="path4471"
d="m 354.9038,193.4519 a 22,22 0 0 1 -22,-22 V -8.5480978 a 22,22 0 0 1 44,0 V 171.4519 a 22,22 0 0 1 -22,22 z" /></g></svg>

Before

Width:  |  Height:  |  Size: 3.1 KiB