Skip to content

Save a core font under a hashed name when another font's file holds its name - #2016

Merged
jakejackson1 merged 1 commit into
developmentfrom
feature/font-file-conflict-rename
Nov 9, 2026
Merged

jakejackson1 merged 1 commit into
developmentfrom
feature/font-file-conflict-rename

Conversation

@gravitypdf-bot

@gravitypdf-bot gravitypdf-bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Model_Font_Downloader::download() treated any file at a manifest name as installed, so a font family could silently use another font's file, and Model_Font_Sources::get_manifest() merged sources with +=, so a later source could silently get an earlier source's file. Now a file at a manifest name that is another font's is left alone, and the family's own font is downloaded under {stem}-{first 8 of the sha}{ext} (for example Garuda-0abddd35.ttf), worked out from the manifest with nothing stored. mPDF looks for that name first, then the plain name.

  • A file at the name is another font's when its size matches no build the sources know: the manifest's, the legacy manifest's (so upgraded sites download nothing) or a source's legacy hashes.
  • Renders only compare sizes. A file of a known size that isn't the manifest's is found by the background sync.
  • With downloads off, or while a download is failing or backing off, the file at the name stays in use, so a conflict never renders worse than before.
  • When two sources use one name for different files, the later source's file takes the hashed name when the manifest is built.
  • A file a Font Manager record uses stays the record's: the core family of that name takes the hashed name instead.
  • A font upload can't take a name a core font's copy could have, and is saved under a numbered name instead. A file of the wrong size at a hashed name isn't used.
  • The Font Manager's rows, sizes and previews show the hashed copy when there is one, and deleting the family removes only that copy.
  • Behaviour change: a core font someone replaced by hand, with no Font Manager record, is now a conflict. The family downloads and uses the real font, and the replacement stays on disk unused. With downloads off, the replacement stays in use.

Suggested changelog line: * Fix: a core font is now saved under a new name when a different file already has its name, instead of silently using that file. A core font replaced by hand is no longer used while downloads are on.

Try it

  1. Replace wp-content/uploads/PDF_EXTENDED_TEMPLATES/fonts/DejaVuSansCondensed.ttf with any other .ttf (or one byte longer), and make a PDF in DejaVu Sans Condensed.
  2. DejaVuSansCondensed-<8 hex>.ttf appears beside your file, the PDF is set in the real DejaVu Sans Condensed, and your file is untouched.
  3. Turn downloads off (define( 'GPDF_AUTO_INSTALL_FONTS', false );), delete the new copy and render again: your file is used and nothing is requested.

Test plan

More info

Detection

Where What it does Cost
Render (On_Demand_Font_File_Finder) is_file() on the hashed name, then the plain name, then a size compare against the manifest, and only on a mismatch the legacy manifest and legacy sizes. A size conflict downloads the font to the hashed name (or keeps the plain file when that can't happen) A few stat calls, plus an opcache include of the manifest for a font in the fonts folder
Background sync (sync()) Every required file with no hashed copy goes through download( $file, true ), which hashes it once and, if its SHA-1 isn't a known build's, downloads the real font to the hashed name. Skipped with downloads off, as nothing could be done about the result. Files that need a download go first, so hashing the rest can't push one into a paused run One git-blob SHA-1 per required file per sync run (measured in the Docker container: 0.7 MB 1 ms, 5.8 MB 8 ms, 23 MB 32 ms), inside the sync's 20 s budget. Nothing records a file as checked, so each sync run hashes again
Retries (get_retries()) A due retry hashes its file once to decide whether it's still unresolved Only files with a failure record

A sync runs on install, a version change, a language pack install and a saved default font or PDF that needs a download. A same-size, different-hash file is therefore found at the next of those, not when it's first put there. Fonts that aren't in the required set (a substitute downloaded mid-render) are only checked by size.

An existing file is never hashed to detect a conflict during a render. The one hash that can happen there is after a conflict download, when an older release's hashed copy is checked before it's removed.

Behaviour beyond the brief

  • Model_Font_Sources::is_downloaded() is still "the file at its name exists" (it runs on every render for Sun-ExtA/ExtB). A new is_installed() ("its own copy is on disk") drives needs-download, the System Report's installed counts and the sync.
  • Saving a hashed copy calls FlushCache::flush() (metrics and the PDF cache generation), as the file the metrics were built from has changed.
  • A new manifest version changes the sha, so the old hashed copy would be orphaned: saving a new hashed copy removes other {stem}-{8 hex}{ext} files whose own SHA-1 starts with the 8 hex in their name (a font someone else put there won't). Until the new copy downloads, a render uses the file at the name.
  • Reserved names cover every {manifest stem}-{8 hex}{ext} in any case, so a later release's hashed name is covered too. A retry of a reserved name uses _ before its number, as digits alone could complete an 8-hex suffix.
  • Model_Font_Downloader::download() takes an optional $verify second argument. New public methods on Model_Font_Sources (get_plain_path, get_hashed_name, parse_hashed_name, is_reserved_name, is_hashed_copy, has_hashed_copy, is_installed, is_size_conflict, get_known_builds, get_record_collisions, is_record_file, use_hashed_names) and Model_Font_Downloader::is_in_fonts_folder(). No constructor changes.
  • Test changes: a test that puts a stand-in file at a core font's name now lists it in the manifest (list_fixture_as()), as it would otherwise be a conflict. The "render with fonts on disk doesn't load the manifest" test is replaced: the manifest is read when a font in the fonts folder is requested, and not for the bundled fonts.

Font Manager records

  • get_record_collisions() (per blog, memoised): a record's file whose basename is a downloadable family's file name, and whose size matches no known build of it, maps to the hashed name. Records are read from the settings already in memory; the manifest is read and a file stat'd only when a record's file has a family's name, and nothing is hashed.
  • The families' font data, the source manifests, the required set and the Font Manager list use the hashed name for those files, so the sync downloads it and deleting the core family removes only it. The finder returns a record's file as is. Deleting a record removes its own file, never the hashed copy.
  • Multisite: records are per site while the folder is shared. Each site protects its own records; a record on another site isn't read (that would be one options query per site per render). That site's own renders are protected by the same rule, and this site's family simply uses the hashed copy.

Known limits

  • Another font's file at the name with no hashed copy yet still lists as the family's core font in the Font Manager, with that file's size, and deleting the family deletes it (delete is deliberately "whatever its hash"). Once the hashed copy exists, delete removes only the copy.
  • A record whose file has a core font's name and the same size as a known build, but other bytes, isn't told apart without hashing, so it counts as the core font. The sync's check can then save a hashed copy, and the plain name's lookup returns it in place of the record's file.
  • A record added after a render is only seen on the next request.
  • A hashed copy with the file at the name deleted is found by the finder, but is_downloaded() (and so the Sun-ExtA substitution check) only sees the file at the name.
  • A family that takes a hashed name from a record collision doesn't remove the older release's copy when a new manifest changes the sha, as that download isn't a conflict save.
  • Conflict downloads at render time are synchronous, like any missing font; Fetch large font files in the background when a PDF needs them #2014's "large files in the background" will need to cover them.
  • The same-size detection only covers the required set.
  • rename_colliding_files() needs two sources with manifests, which only exist after Let plugins add font sources through the gfpdf_font_sources filter #2013, so it's exercised in tests with a test subclass until then.

Open PRs based on this branch

#2008, #2010, #2011, #2012, #2013 and #2014 target this branch and need rebasing onto the new commit.

Baseline failures

test_an_unwritable_folder_fails_without_a_request (x2) needs chmod to refuse the owner, and the container runs as root, so it cannot pass in it.

@github-actions

github-actions Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Coverage report for commit: 38e02af
File: ./tmp/jest-coverage/clover.xml

Cover ┌─────────────────────────┐ Freq.
   0% │ ░░░░░░░░░░░░░░░░░░░░░░░ │  0.0%
  10% │ ░░░░░░░░░░░░░░░░░░░░░░░ │  0.0%
  20% │ ░░░░░░░░░░░░░░░░░░░░░░░ │  0.0%
  30% │ ░░░░░░░░░░░░░░░░░░░░░░░ │  0.0%
  40% │ ░░░░░░░░░░░░░░░░░░░░░░░ │  0.0%
  50% │ ░░░░░░░░░░░░░░░░░░░░░░░ │  0.0%
  60% │ ░░░░░░░░░░░░░░░░░░░░░░░ │  0.0%
  70% │ █░░░░░░░░░░░░░░░░░░░░░░ │  3.1%
  80% │ ██░░░░░░░░░░░░░░░░░░░░░ │  4.7%
  90% │ ██████░░░░░░░░░░░░░░░░░ │ 18.8%
 100% │ ███████████████████████ │ 73.4%
      └─────────────────────────┘
 *Legend:* █ = Current Distribution 
Summary - Lines: 93.99% | Methods: 90.96% | Branches: 85.66%
FilesLinesMethodsBranches
src/assets/js/react/actions
   fontManager.js97.92%91.67%100.00%
   templates.js100.00%100.00%100.00%
src/assets/js/react/components/Alert
   Alert.js100.00%100.00%100.00%
src/assets/js/react/components
   CustomHashRouter.js100.00%100.00%100.00%
   Empty.js100.00%100.00%100.00%
   ShowMessage.js80.65%80.00%72.22%
   Spinner.js100.00%100.00%100.00%
src/assets/js/react/components/FontManager
   AddFont.js100.00%100.00%100.00%
   AddUpdateFontFooter.js100.00%100.00%91.67%
   AdvancedButton.js100.00%100.00%100.00%
   DeleteConfirm.js90.91%75.00%100.00%
   FontDeleteButton.js100.00%80.00%100.00%
   FontDetail.js100.00%100.00%100.00%
   FontInstallButton.js100.00%66.67%100.00%
   FontList.js100.00%50.00%65.22%
   FontListAlertMessage.js100.00%100.00%100.00%
   FontListItems.js96.00%83.33%88.24%
   FontListSkeleton.js100.00%100.00%100.00%
   FontManager.js73.68%50.00%50.00%
   FontManagerBody.js89.12%90.32%84.96%
   FontManagerHeader.js100.00%100.00%25.00%
   FontPreview.js100.00%100.00%100.00%
   FontStatus.js100.00%66.67%100.00%
   FontUsage.js100.00%66.67%100.00%
   FontVariant.js94.12%80.00%90.24%
   FontVariantLabel.js100.00%100.00%100.00%
   Icon.js100.00%100.00%100.00%
   InitialAddUpdateState.js100.00%100.00%100.00%
   SearchBox.js90.00%66.67%69.23%
   TemplateTooltip.js100.00%100.00%100.00%
   UpdateFont.js75.00%50.00%64.29%
src/assets/js/react/components/Modal
   CloseDialog.js93.33%66.67%70.59%
src/assets/js/react/components/Template
   TemplateActivateButton.js100.00%100.00%100.00%
   TemplateButton.js85.71%66.67%100.00%
   TemplateContainer.js71.43%66.67%25.00%
   TemplateDeleteButton.js100.00%100.00%70.00%
   TemplateDocsLink.js100.00%100.00%100.00%
   TemplateFooterActions.js100.00%100.00%100.00%
   TemplateHeaderNavigation.js82.35%85.71%70.00%
   TemplateHeaderTitle.js100.00%100.00%100.00%
   TemplateList.js100.00%100.00%100.00%
   TemplateListItem.js100.00%100.00%92.86%
   TemplateListItemComponents.js100.00%100.00%100.00%
   TemplateManagerClosed.js100.00%100.00%100.00%
   TemplateScreenshot.js100.00%100.00%100.00%
   TemplateScreenshots.js100.00%100.00%50.00%
   TemplateSearch.js93.75%88.89%50.00%
   TemplateSingle.js100.00%100.00%100.00%
   TemplateSingleComponents.js100.00%100.00%75.00%
   TemplateUploader.js100.00%100.00%95.00%
   TemplateUploaderContext.js100.00%100.00%100.00%
   TemplateUploaderTile.js100.00%100.00%100.00%
src/assets/js/react/reducers
   fontManagerReducer.js91.46%100.00%81.08%
   index.js100.00%100.00%100.00%
   templateReducer.js100.00%100.00%100.00%
src/assets/js/react/sagas
   fontManager.js90.16%92.86%88.57%
   index.js100.00%100.00%100.00%
   templates.js91.30%100.00%100.00%
src/assets/js/react/selectors
   getTemplates.js91.11%100.00%83.33%
src/assets/js/react/utilities/FontManager
   associatedFontManagerSelectBox.js94.44%100.00%77.78%
   fontManagerReducer.js100.00%100.00%94.12%
   toggleUpdateFont.js100.00%100.00%100.00%
src/assets/js/react/utilities/Template
   hasPendingUpload.js100.00%100.00%100.00%
src/assets/js/react/utilities
   withRouterHooks.js100.00%100.00%100.00%

🤖 Jest coverage report

@github-actions

github-actions Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Coverage report for commit: 38e02af
File: tmp/coverage/report-xml/merged.xml

Cover ┌─────────────────────────┐ Freq.
   0% │ ██████░░░░░░░░░░░░░░░░░ │  7.6%
  10% │ ░░░░░░░░░░░░░░░░░░░░░░░ │  0.0%
  20% │ █░░░░░░░░░░░░░░░░░░░░░░ │  0.4%
  30% │ █░░░░░░░░░░░░░░░░░░░░░░ │  1.3%
  40% │ █░░░░░░░░░░░░░░░░░░░░░░ │  0.4%
  50% │ ███████░░░░░░░░░░░░░░░░ │  9.3%
  60% │ █░░░░░░░░░░░░░░░░░░░░░░ │  0.9%
  70% │ ██░░░░░░░░░░░░░░░░░░░░░ │  2.7%
  80% │ █████████████░░░░░░░░░░ │ 16.9%
  90% │ ███████████████████████ │ 31.6%
 100% │ ██████████████████████░ │ 28.9%
      └─────────────────────────┘
 *Legend:* █ = Current Distribution 
Summary - Lines: 89.64% | Methods: 95.78%
FilesLinesMethodsBranches
/var/www/html/wp-content/plugins/gravity-pdf
   api.php98.14%100.00%100.00%
   gravity-pdf-updater.php53.97%100.00%100.00%
   pdf.php59.82%81.25%100.00%
/var/www/html/wp-content/plugins/gravity-pdf/src/Controller
   Controller_Actions.php100.00%100.00%100.00%
   Controller_Activation.php95.83%100.00%100.00%
   Controller_Custom_Fonts.php95.52%100.00%100.00%
   Controller_Debug.php100.00%100.00%100.00%
   Controller_Export_Entries.php96.77%100.00%100.00%
   Controller_Font_Downloader.php100.00%100.00%100.00%
   Controller_Font_Sources.php100.00%100.00%100.00%
   Controller_Form_Settings.php87.37%90.00%100.00%
   Controller_Install.php100.00%100.00%100.00%
   Controller_Mergetags.php100.00%100.00%100.00%
   Controller_PDF.php81.46%100.00%100.00%
   Controller_Pdf_Cache.php100.00%100.00%100.00%
   Controller_Pdf_Queue.php85.27%87.50%100.00%
   Controller_Save_Core_Fonts.php91.67%100.00%100.00%
   Controller_Settings.php86.90%100.00%100.00%
   Controller_Shortcodes.php100.00%100.00%100.00%
   Controller_System_Report.php100.00%100.00%100.00%
   Controller_Templates.php100.00%100.00%100.00%
   Controller_Uninstaller.php83.33%77.78%100.00%
   Controller_Upgrade_Routines.php95.19%100.00%100.00%
   Controller_Webhooks.php100.00%100.00%100.00%
   Controller_Zapier.php100.00%100.00%100.00%
/var/www/html/wp-content/plugins/gravity-pdf/src/Exceptions
   GravityPdfDatabaseUpdateException.php50.00%100.00%100.00%
   GravityPdfDomainException.php50.00%100.00%100.00%
   GravityPdfException.php50.00%100.00%100.00%
   GravityPdfFontNotFoundException.php50.00%100.00%100.00%
   GravityPdfIdException.php50.00%100.00%100.00%
   GravityPdfModelNotUpdatedException.php50.00%100.00%100.00%
   GravityPdfNotATemplateException.php50.00%100.00%100.00%
   GravityPdfRuntimeException.php50.00%100.00%100.00%
   GravityPdfShortcodeEntryIdException.php50.00%100.00%100.00%
   GravityPdfShortcodePdfConditionalLogicFailedException.php50.00%100.00%100.00%
   GravityPdfShortcodePdfConfigNotFoundException.php50.00%100.00%100.00%
   GravityPdfShortcodePdfInactiveException.php50.00%100.00%100.00%
/var/www/html/wp-content/plugins/gravity-pdf/src/Helper/Fields
   Field_Address.php92.86%100.00%100.00%
   Field_Chainedselect.php66.67%75.00%100.00%
   Field_Checkbox.php94.44%100.00%100.00%
   Field_Consent.php95.00%100.00%100.00%
   Field_Coupon.php--100.00%
   Field_Creditcard.php83.33%100.00%100.00%
   Field_Date.php83.33%100.00%100.00%
   Field_Default.php83.33%100.00%100.00%
   Field_Discount.php44.00%75.00%100.00%
   Field_Email.php83.33%100.00%100.00%
   Field_Fg_Ls_Consent.php92.86%100.00%100.00%
   Field_Fg_Ls_Signature.php73.08%66.67%100.00%
   Field_Fileupload.php95.83%100.00%100.00%
   Field_Form.php90.91%100.00%100.00%
   Field_Hidden.php81.82%100.00%100.00%
   Field_Html.php89.47%100.00%100.00%
   Field_Image_Choice.php86.67%100.00%100.00%
   Field_Likert.php97.22%100.00%100.00%
   Field_List.php92.41%100.00%100.00%
   Field_Multi_Choice.php50.00%100.00%100.00%
   Field_Multiselect.php92.59%100.00%100.00%
   Field_Name.php84.62%100.00%100.00%
   Field_Number.php83.33%100.00%100.00%
   Field_Option.php96.15%100.00%100.00%
   Field_Page.php83.33%100.00%100.00%
   Field_Phone.php94.55%100.00%100.00%
   Field_Poll.php93.75%100.00%100.00%
   Field_Post_Category.php85.00%100.00%100.00%
   Field_Post_Content.php82.35%100.00%100.00%
   Field_Post_Custom_Field.php50.00%100.00%100.00%
   Field_Post_Excerpt.php81.82%100.00%100.00%
   Field_Post_Image.php94.00%100.00%100.00%
   Field_Post_Tags.php90.91%100.00%100.00%
   Field_Post_Title.php81.82%100.00%100.00%
   Field_Product.php92.31%100.00%100.00%
   Field_Products.php84.32%100.00%100.00%
   Field_Quantity.php84.62%100.00%100.00%
   Field_Quiz.php89.74%100.00%100.00%
   Field_Radio.php94.74%100.00%100.00%
   Field_Rank.php95.24%100.00%100.00%
   Field_Rating.php95.24%100.00%100.00%
   Field_Repeater.php97.39%100.00%100.00%
   Field_Section.php90.74%100.00%100.00%
   Field_Select.php94.12%100.00%100.00%
   Field_Shipping.php92.86%100.00%100.00%
   Field_Signature.php71.11%100.00%100.00%
   Field_Slim.php82.35%100.00%100.00%
   Field_Slim_Post.php91.30%100.00%100.00%
   Field_Subtotal.php65.38%75.00%100.00%
   Field_Survey.php95.00%100.00%100.00%
   Field_Tax.php32.00%50.00%100.00%
   Field_Text.php81.82%100.00%100.00%
   Field_Textarea.php91.18%100.00%100.00%
   Field_Time.php81.82%100.00%100.00%
   Field_Tos.php92.86%100.00%100.00%
   Field_Total.php88.00%100.00%100.00%
   Field_Website.php85.71%100.00%100.00%
/var/www/html/wp-content/plugins/gravity-pdf/src/Helper/Fonts
   FlushCache.php92.31%100.00%100.00%
   LocalFile.php88.89%100.00%100.00%
   LocalFilesystem.php66.67%100.00%100.00%
   SupportsOtl.php87.50%100.00%100.00%
   TtfFontValidation.php84.62%100.00%100.00%
/var/www/html/wp-content/plugins/gravity-pdf/src/Helper
   Helper_Abstract_Addon.php94.09%100.00%100.00%
   Helper_Abstract_Config_Settings.php75.00%100.00%100.00%
   Helper_Abstract_Controller.php-100.00%100.00%
   Helper_Abstract_Field_Products.php89.47%66.67%100.00%
   Helper_Abstract_Fields.php96.10%95.83%100.00%
   Helper_Abstract_Fields_Input_Type.php84.00%100.00%100.00%
   Helper_Abstract_Form.php--100.00%
   Helper_Abstract_Model.php100.00%100.00%100.00%
   Helper_Abstract_Options.php77.13%79.66%100.00%
   Helper_Abstract_Pdf_Shortcode.php94.67%95.65%100.00%
   Helper_Abstract_View.php96.77%100.00%100.00%
   Helper_Async_Notifications.php64.29%100.00%100.00%
   Helper_Data.php98.43%100.00%100.00%
   Helper_Field_Container.php92.31%100.00%100.00%
   Helper_Field_Container_Gf25.php92.00%100.00%100.00%
   Helper_Field_Container_Void.php16.67%-100.00%
   Helper_Form.php80.65%64.29%100.00%
   Helper_Interface_Actions.php-100.00%100.00%
   Helper_Interface_Config.php50.00%100.00%100.00%
   Helper_Interface_Config_Settings.php50.00%100.00%100.00%
   Helper_Interface_Deprecated_Features.php50.00%100.00%100.00%
   Helper_Interface_Extension_Settings.php-100.00%100.00%
   Helper_Interface_Extension_Uninstaller.php-100.00%100.00%
   Helper_Interface_Field_Pdf_Config.php50.00%100.00%100.00%
   Helper_Interface_Filters.php-100.00%100.00%
   Helper_Interface_Setup_TearDown.php50.00%100.00%100.00%
   Helper_Interface_Url_Signer.php-100.00%100.00%
   Helper_Logger.php-100.00%100.00%
   Helper_Misc.php89.17%100.00%100.00%
   Helper_Mpdf.php50.00%100.00%100.00%
   Helper_Notices.php92.98%100.00%100.00%
   Helper_Options_Fields.php98.10%100.00%100.00%
   Helper_PDF.php88.61%97.56%100.00%
   Helper_PDF_List_Table.php84.50%92.31%100.00%
   Helper_Pdf_Queue.php84.31%100.00%100.00%
   Helper_QueryPath.php80.00%100.00%100.00%
   Helper_Render_Health.php85.71%100.00%100.00%
   Helper_Sha256_Url_Signer.php90.91%100.00%100.00%
   Helper_Singleton.php100.00%100.00%100.00%
   Helper_Templates.php97.29%100.00%100.00%
   Helper_Trait_Logger.php25.00%-100.00%
   Helper_Trait_Removed_Methods.php100.00%100.00%100.00%
   Helper_Url_Signer.php84.82%100.00%100.00%
/var/www/html/wp-content/plugins/gravity-pdf/src/Helper/Licensing
Table truncated to fit comment

🤖 PHPUnit coverage report

…ts name

download() returned true for any file at a manifest name, so a family could silently use another font's file, and
merging two sources' manifests with += gave the later source the first's file for a name they use for different files.

A file at a manifest name is another font's when its size matches no build the sources know (the manifest's, the
legacy manifest's or a source's `legacy` hashes). The family's font is then saved as {stem}-{first 8 of the sha}{ext},
derived from the manifest with nothing stored, and mPDF looks for that name first. The file at the name is never
touched. A size mismatch is a stat call, so it is also found while rendering; a file of a known size that is not the
manifest's is found by the background sync, the only place a file already on disk is hashed. With downloads off, or a
download failing or backing off, the file at the name stays in use, so a conflict never renders worse than before.

Two sources that use one name for different files get it at merge time: the later source's manifest entry and family
file references take the hashed name, so nothing downstream sees a duplicate.

A font upload can't take a name a core font's copy could have, as that copy is used before the file at the plain name,
and a file of the wrong size at the hashed name isn't treated as the copy.

Every reader of a font file's path now agrees: the finder, the downloaded fonts list and delete (a hashed copy is
deleted alone), the installed counts and needs-download checks, the sync's file list and retries, prune and adoption
(which read the file at the name), and the unregistered fonts auto-loader (which skips hashed copies). Saving a hashed
copy flushes the font metrics and removes a copy left by an older release of the font.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@gravitypdf-bot
gravitypdf-bot force-pushed the feature/font-file-conflict-rename branch from 62cbe50 to 38e02af Compare November 9, 2026 06:09
@jakejackson1
jakejackson1 merged commit 57870af into development Nov 9, 2026
@jakejackson1
jakejackson1 deleted the feature/font-file-conflict-rename branch November 9, 2026 06:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants